diff --git a/ForwardDetectors/ZDC/ZdcAnalysis/CMakeLists.txt b/ForwardDetectors/ZDC/ZdcAnalysis/CMakeLists.txt
index 166a31ed57aaaa8f56bcef3b9ae4c660b228210a..5d264fdd2efae82b371adc640df134ce22cd59fa 100644
--- a/ForwardDetectors/ZDC/ZdcAnalysis/CMakeLists.txt
+++ b/ForwardDetectors/ZDC/ZdcAnalysis/CMakeLists.txt
@@ -12,7 +12,7 @@ atlas_add_library( ZdcAnalysisLib
                    PUBLIC_HEADERS ZdcAnalysis
                    INCLUDE_DIRS ${ROOT_INCLUDE_DIRS}
                    LINK_LIBRARIES ${ROOT_LIBRARIES} AsgDataHandlesLib AsgTools CxxUtils xAODEventInfo xAODForward xAODTrigL1Calo
-		           PRIVATE_LINK_LIBRARIES PathResolver )
+		                   PRIVATE_LINK_LIBRARIES PathResolver )
 
 atlas_add_component( ZdcAnalysis
 		             src/components/*.cxx
diff --git a/ForwardDetectors/ZDC/ZdcRec/CMakeLists.txt b/ForwardDetectors/ZDC/ZdcRec/CMakeLists.txt
index 06f389fad1be09ac5841aa5e53e00d05e90aa7c4..8de1b11b3b9611ab33e591806ee83f64ba502602 100644
--- a/ForwardDetectors/ZDC/ZdcRec/CMakeLists.txt
+++ b/ForwardDetectors/ZDC/ZdcRec/CMakeLists.txt
@@ -13,7 +13,7 @@ atlas_add_library( ZdcRecLib
                    PUBLIC_HEADERS ZdcRec
                    INCLUDE_DIRS ${GSL_INCLUDE_DIRS}
                    PRIVATE_INCLUDE_DIRS ${ROOT_INCLUDE_DIRS}
-                   LINK_LIBRARIES ${GSL_LIBRARIES} AsgTools AthenaBaseComps xAODForward xAODTrigL1Calo ZdcEvent GaudiKernel StoreGateLib ZdcAnalysisLib ZdcByteStreamLib
+                   LINK_LIBRARIES ${GSL_LIBRARIES} AsgTools AthenaBaseComps xAODForward xAODTrigL1Calo ZdcEvent GaudiKernel StoreGateLib ZdcAnalysisLib ZdcByteStreamLib ZdcTrigValidLib
                    PRIVATE_LINK_LIBRARIES ${ROOT_LIBRARIES} CxxUtils ZdcByteStreamLib ZdcConditions ZdcIdentifier )
 
 atlas_add_component( ZdcRec
diff --git a/ForwardDetectors/ZDC/ZdcRec/ZdcRec/ZdcRecRun3.h b/ForwardDetectors/ZDC/ZdcRec/ZdcRec/ZdcRecRun3.h
index 828803c5db9e0f88ed2608a1dd05ebd7b1e29e5d..505aebdd534b0477b001fd1c8422201b54b9a25c 100755
--- a/ForwardDetectors/ZDC/ZdcRec/ZdcRec/ZdcRecRun3.h
+++ b/ForwardDetectors/ZDC/ZdcRec/ZdcRec/ZdcRecRun3.h
@@ -28,6 +28,7 @@ class ZdcRecChannelToolLucrod;
 #include "xAODForward/ZdcModuleContainer.h"
 #include "xAODForward/ZdcModuleAuxContainer.h"
 #include "ZdcAnalysis/ZdcAnalysisTool.h"
+#include "ZdcTrigValid/ZdcTrigValidTool.h"
 
 /** @class ZdcRecRun3
 
@@ -64,7 +65,8 @@ private:
 
 	ToolHandle<ZdcRecChannelToolLucrod> m_ChannelTool
 	  { this, "ChannelTool", "ZdcRecChannelToolLucrod", "" };
-
+	ToolHandle<ZDC::IZdcTrigValidTool> m_trigValTool 
+	  { this, "TrigValid", "ZdcTrigValidTool",""};
 	ToolHandleArray<ZDC::IZdcAnalysisTool> m_zdcTools{ this, "ZdcAnalysisTools",{} };
 
 
diff --git a/ForwardDetectors/ZDC/ZdcRec/python/ZdcRecConfig.py b/ForwardDetectors/ZDC/ZdcRec/python/ZdcRecConfig.py
index 824e7c1f3c78fac9413e1ac2c9251201cc9bc631..840796291e0511be08b9bff223689c666913ebf5 100644
--- a/ForwardDetectors/ZDC/ZdcRec/python/ZdcRecConfig.py
+++ b/ForwardDetectors/ZDC/ZdcRec/python/ZdcRecConfig.py
@@ -14,6 +14,9 @@ from OutputStreamAthenaPool.OutputStreamConfig import addToESD
 
 from TriggerJobOpts.TriggerByteStreamConfig import ByteStreamReadCfg
 
+from TrigConfigSvc.TriggerConfigAccess import getL1MenuAccess
+from TrigDecisionTool.TrigDecisionToolConfig import TrigDecisionToolCfg
+
 def ZdcRecOutputCfg(flags):
 
     acc = ComponentAccumulator()
@@ -45,6 +48,26 @@ def ZdcAnalysisToolCfg(flags, run, config="LHCf2022", DoCalib=False, DoTimeCalib
         LHCRun = run ))
     return acc
 
+
+def ZdcTrigValToolCfg(flags, config = 'LHCf2022'):
+    acc = ComponentAccumulator()
+    
+    acc.merge(TrigDecisionToolCfg(flags))
+    
+    trigValTool = CompFactory.ZDC.ZdcTrigValidTool(
+        name = 'ZdcTrigValTool',
+        WriteAux = True,
+        AuxSuffix = "",
+        filepath_LUT = "TrigT1ZDC/zdcRun3T1LUT_v1_30_05_2023.json")
+        
+    trigValTool.TrigDecisionTool = acc.getPublicTool('TrigDecisionTool')
+    
+    trigValTool.triggerList = [c for c in getL1MenuAccess(flags) if 'L1_ZDC_BIT' in c]
+    
+    acc.setPrivateTools(trigValTool)
+      
+    return acc
+
 def ZdcRecRun2Cfg(flags):        
     acc = ComponentAccumulator()
     config = "default"
@@ -95,16 +118,21 @@ def ZdcRecRun3Cfg(flags):
         config = "LHCf2022"
     elif flags.Input.ProjectName == "data23_hi":
         config = "PbPb2023"
+    elif flags.Input.ProjectName == "data22_hi":
+        config = "PbPb2023"
 
     acc.merge(ByteStreamReadCfg(flags, type_names=['xAOD::TriggerTowerContainer/ZdcTriggerTowers',
                                          'xAOD::TriggerTowerAuxContainer/ZdcTriggerTowersAux.']))
 
     acc.addEventAlgo(CompFactory.ZdcByteStreamLucrodData())
     anaTool = acc.popToolsAndMerge(ZdcAnalysisToolCfg(flags,3,config,doCalib,doTimeCalib,doTrigEff))
+    trigTool = acc.popToolsAndMerge(ZdcTrigValToolCfg(flags,config))
+    
+    zdcTools = [] # expand list as needed
+    zdcTools += [anaTool] 
 
-    zdcTools = [anaTool] # expand list as needed
-
-    zdcAlg = CompFactory.ZdcRecRun3("ZdcRecRun3",ZdcAnalysisTools=zdcTools)
+    
+    zdcAlg = CompFactory.ZdcRecRun3("ZdcRecRun3",ZdcAnalysisTools=zdcTools, TrigValid = trigTool)
     acc.addEventAlgo(zdcAlg, primary=True)
 
     return acc
diff --git a/ForwardDetectors/ZDC/ZdcRec/src/ZdcRecRun3.cxx b/ForwardDetectors/ZDC/ZdcRec/src/ZdcRecRun3.cxx
index 4e1e658fdebff44598120579430789186773a3ef..11c880d9ebdf9051c099c4ed9885d78fc6c61d8e 100644
--- a/ForwardDetectors/ZDC/ZdcRec/src/ZdcRecRun3.cxx
+++ b/ForwardDetectors/ZDC/ZdcRec/src/ZdcRecRun3.cxx
@@ -46,6 +46,9 @@ StatusCode ZdcRecRun3::initialize()
 
 	// Reconstruction Tool
 	ATH_CHECK( m_ChannelTool.retrieve() );
+  
+  // Trigger Validation Tool
+  ATH_CHECK( m_trigValTool.retrieve() );
 
 	// Reconstruction Tool
 
@@ -117,6 +120,9 @@ StatusCode ZdcRecRun3::execute()
 
   // eventually reconstruct RPD, using ML libraries
   // ATH_CHECK( m_rpdTool...)
+  
+  //Use Trigger Validaiton Tool
+  ATH_CHECK( m_trigValTool->addTrigStatus(*moduleContainer.get(), *moduleSumContainer.get()));
 
   SG::WriteHandle<xAOD::ZdcModuleContainer> moduleContainerH (m_zdcModuleContainerName, ctx);
   ATH_CHECK( moduleContainerH.record (std::move(moduleContainer),
diff --git a/ForwardDetectors/ZDC/ZdcTrigValid/CMakeLists.txt b/ForwardDetectors/ZDC/ZdcTrigValid/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..dba75310ba9dc9591d8794aead1892c623d1a33e
--- /dev/null
+++ b/ForwardDetectors/ZDC/ZdcTrigValid/CMakeLists.txt
@@ -0,0 +1,20 @@
+# Copyright (C) 2002-2023 CERN for the benefit of the ATLAS collaboration
+
+# Declare the package name:
+atlas_subdir( ZdcTrigValid )
+
+# External dependencies:
+find_package( ROOT COMPONENTS Core Tree MathCore Hist RIO Minuit Minuit2 HistPainter Graf )
+
+# Component(s) in the package:
+atlas_add_library( ZdcTrigValidLib
+                   Root/*.cxx
+                   PUBLIC_HEADERS ZdcTrigValid
+                   INCLUDE_DIRS ${ROOT_INCLUDE_DIRS}
+                   LINK_LIBRARIES  ${ROOT_LIBRARIES} AsgDataHandlesLib AsgTools TrigDecisionInterface TrigDecisionToolLib CxxUtils xAODEventInfo xAODForward xAODTrigL1Calo ZdcUtilsLib
+		               PRIVATE_LINK_LIBRARIES PathResolver )
+
+atlas_add_component( ZdcTrigValid
+                     src/components/*.cxx
+                     LINK_LIBRARIES ZdcTrigValidLib )
+
diff --git a/ForwardDetectors/ZDC/ZdcTrigValid/Root/ZdcTrigValidTool.cxx b/ForwardDetectors/ZDC/ZdcTrigValid/Root/ZdcTrigValidTool.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..8d8fe6fc8f8c23bd3da3dc5b243e1bfb487e787c
--- /dev/null
+++ b/ForwardDetectors/ZDC/ZdcTrigValid/Root/ZdcTrigValidTool.cxx
@@ -0,0 +1,133 @@
+/*
+  Copyright (C) 2002-2023 CERN for the benefit of the ATLAS collaboration
+*/
+
+#include "ZdcTrigValid/ZdcTrigValidTool.h"
+#include "PathResolver/PathResolver.h"
+#include <fstream>
+#include <bitset>
+#include <stdexcept>
+
+using json = nlohmann::json;
+
+//**********************************************************************
+namespace ZDC{
+ZdcTrigValidTool::ZdcTrigValidTool(const std::string& name)
+ : asg::AsgTool(name), m_name(name) 
+ {
+  
+#ifndef XAOD_STANDALONE
+  declareInterface<IZdcTrigValidTool>(this);
+#endif
+  declareProperty("Message", m_msg = "");
+  declareProperty("WriteAux", m_writeAux = true);
+  declareProperty("AuxSuffix", m_auxSuffix = "");
+}
+
+//**********************************************************************
+
+ZdcTrigValidTool::~ZdcTrigValidTool()
+{
+    ATH_MSG_DEBUG("Deleting ZdcTrigValidTool named " << m_name);
+}
+
+StatusCode ZdcTrigValidTool::initialize() {
+
+  ATH_MSG_INFO("Initialising tool " << m_name);
+  ATH_CHECK(m_trigDecTool.retrieve());
+  ATH_MSG_INFO("TDT retrieved");
+  
+  // Find the full path to filename:
+  std::string file = PathResolverFindCalibFile(m_lutFile);
+  ATH_MSG_INFO("Reading file " << file);
+  std::ifstream fin(file.c_str());
+  if(!fin){
+     ATH_MSG_ERROR("Can not read file: " << file);
+     return StatusCode::FAILURE;
+  }
+  json data = json::parse(fin);
+   
+  // Obtain LUTs from Calibration Area
+  // A data member to hold the side A LUT values
+  std::array<unsigned int, 4096> sideALUT = data["LucrodLowGain"]["LUTs"]["sideA"];
+  // A data member to hold the side C LUT values
+  std::array<unsigned int, 4096> sideCLUT = data["LucrodLowGain"]["LUTs"]["sideC"];
+  // A data member to hold the Combined LUT values
+  std::array<unsigned int, 256> combLUT = data["LucrodLowGain"]["LUTs"]["comb"];
+  
+  //Construct Trigger Map
+  m_triggerMap.insert({"L1_ZDC_BIT0",0});
+  m_triggerMap.insert({"L1_ZDC_BIT1",1});
+  m_triggerMap.insert({"L1_ZDC_BIT2",2});
+   
+  // Construct Simulation Objects
+  m_modInputs_p = std::make_shared<ZDCTriggerSim::ModuleAmplInputsFloat>(ZDCTriggerSim::ModuleAmplInputsFloat());
+  m_simTrig = std::make_shared<ZDCTriggerSimModuleAmpls>(ZDCTriggerSimModuleAmpls(sideALUT, sideCLUT, combLUT));
+  ATH_MSG_INFO(m_name<<" Initialised");
+  
+  return StatusCode::SUCCESS;
+  
+}
+
+
+StatusCode ZdcTrigValidTool::addTrigStatus(const xAOD::ZdcModuleContainer& moduleContainer, const xAOD::ZdcModuleContainer& moduleSumContainer)
+{ 
+  std::vector<float> moduleEnergy = {0., 0., 0., 0., 0., 0., 0., 0.};
+  
+  bool trigMatch = false;
+  for (const auto zdcModule : moduleContainer) {
+    if (zdcModule->zdcType() == 1) continue;
+    
+    // Side A
+    if (zdcModule->zdcSide() > 0) {
+      moduleEnergy.at(zdcModule->zdcModule()) =
+          zdcModule->auxdataConst<float>("Amplitude" + m_auxSuffix);
+    }
+
+       // Side C
+        if (zdcModule->zdcSide() < 0) {
+      moduleEnergy.at(zdcModule->zdcModule() + 4) =
+          zdcModule->auxdataConst<float>("Amplitude" + m_auxSuffix);
+        }
+  } 
+  // Get Output as an integer (0-7)
+  m_modInputs_p->setData(moduleEnergy);
+   
+  // call ZDCTriggerSim to actually get ZDC Bits
+  unsigned int wordOut = m_simTrig->simLevel1Trig(ZDCTriggerSim::SimDataCPtr(m_modInputs_p));
+
+  // convert int to bitset
+  std::bitset<3> bin(wordOut);
+  
+  // get trigger decision tool
+  const auto &trigDecTool = m_trigDecTool;
+  
+  // iterate through zdc bit output from CTP, validate that they match above bitset
+  for (const auto &trig : m_triggerList)
+    {
+
+      if (m_triggerMap.find(trig) == m_triggerMap.end())
+        continue;
+      if (not trigDecTool->isPassed(trig, TrigDefs::requireDecision)) {
+        ATH_MSG_DEBUG("Chain " << trig << " is passed: NO");
+        if (bin[m_triggerMap[trig]] == 0)
+          trigMatch = true;
+        continue;
+      }
+      ATH_MSG_DEBUG("Chain " << trig << " is passed: YES");
+      if (bin[m_triggerMap[trig]] == 1 )
+        trigMatch = true;
+    }
+
+// write 1 if decision from ZDC firmware matches CTP, 0 otherwize  
+for(const auto zdc_sum : moduleSumContainer){
+  if(m_writeAux) 
+    zdc_sum->auxdecor<unsigned int>("TrigValStatus"+m_auxSuffix) = trigMatch;
+   }
+
+ATH_MSG_DEBUG("ZDC Trigger Status: "
+                 << trigMatch);
+
+return StatusCode::SUCCESS;
+}
+} //namespace ZDC
diff --git a/ForwardDetectors/ZDC/ZdcTrigValid/ZdcTrigValid/ATLAS_CHECK_THREAD_SAFETY b/ForwardDetectors/ZDC/ZdcTrigValid/ZdcTrigValid/ATLAS_CHECK_THREAD_SAFETY
new file mode 100644
index 0000000000000000000000000000000000000000..55b6c7652523b80ff618c303e9ddc47f76c26487
--- /dev/null
+++ b/ForwardDetectors/ZDC/ZdcTrigValid/ZdcTrigValid/ATLAS_CHECK_THREAD_SAFETY
@@ -0,0 +1 @@
+ForwardDetectors/ZDC/ZdcTrigValid
diff --git a/ForwardDetectors/ZDC/ZdcTrigValid/ZdcTrigValid/IZdcTrigValidTool.h b/ForwardDetectors/ZDC/ZdcTrigValid/ZdcTrigValid/IZdcTrigValidTool.h
new file mode 100644
index 0000000000000000000000000000000000000000..54854e5926b45c880d5a601a21baef801570500f
--- /dev/null
+++ b/ForwardDetectors/ZDC/ZdcTrigValid/ZdcTrigValid/IZdcTrigValidTool.h
@@ -0,0 +1,25 @@
+/*
+  Copyright (C) 2002-2023 CERN for the benefit of the ATLAS collaboration
+*/
+
+#ifndef IZDCTRIGVALIDTOOL_H__
+#define IZDCTRIGVALIDTOOL_H__
+
+#include "AsgTools/IAsgTool.h"
+#include "xAODForward/ZdcModuleContainer.h"
+
+namespace ZDC
+{
+  
+class IZdcTrigValidTool : virtual public asg::IAsgTool 
+{
+  ASG_TOOL_INTERFACE( ZDC::IZdcTrigValidTool )
+
+ public:
+ 
+  virtual StatusCode addTrigStatus(const xAOD::ZdcModuleContainer& moduleContainer, const xAOD::ZdcModuleContainer& moduleSumContainer) = 0;
+
+};
+
+}
+#endif
diff --git a/ForwardDetectors/ZDC/ZdcTrigValid/ZdcTrigValid/ZdcTrigValidTool.h b/ForwardDetectors/ZDC/ZdcTrigValid/ZdcTrigValid/ZdcTrigValidTool.h
new file mode 100644
index 0000000000000000000000000000000000000000..ac9cd0ec5efd1201df3516834844a1fc2b1b2465
--- /dev/null
+++ b/ForwardDetectors/ZDC/ZdcTrigValid/ZdcTrigValid/ZdcTrigValidTool.h
@@ -0,0 +1,58 @@
+/*
+  Copyright (C) 2002-2023 CERN for the benefit of the ATLAS collaboration
+*/
+
+#ifndef ZDCTRIGVALID_ZDCTRIGVALIDTOOL_H
+#define ZDCTRIGVALID_ZDCTRIGVALIDTOOL_H
+
+// Matthew Hoppesch.
+// July 2023
+//
+// This is a simple tool to compare L1 ZDC decions made in CTP to those made using ZDC firmware
+// Used for validation of ZDC firmware in Run3, 
+// WriteAux Property Determines if Trigger Validation Status is written to xAOD::ZdcModuleContainer
+
+#include <xAODForward/ZdcModuleAuxContainer.h>
+#include "ZdcTrigValid/IZdcTrigValidTool.h"
+#include "AsgTools/AsgTool.h"
+#include "TrigDecisionTool/TrigDecisionTool.h"
+#include "ZdcUtils/ZDCTriggerSim.h"
+
+#include "nlohmann/json.hpp"
+
+namespace ZDC {
+class ATLAS_NOT_THREAD_SAFE ZdcTrigValidTool : public virtual IZdcTrigValidTool, public asg::AsgTool 
+{
+  ASG_TOOL_CLASS(ZdcTrigValidTool, ZDC::IZdcTrigValidTool)
+
+ public:
+  ZdcTrigValidTool(const std::string& name);
+  virtual ~ZdcTrigValidTool() override;
+  StatusCode initialize() override;
+
+  StatusCode addTrigStatus(const xAOD::ZdcModuleContainer& moduleContainer, const xAOD::ZdcModuleContainer& moduleSumContainer) override;
+
+ protected:
+  PublicToolHandle<Trig::TrigDecisionTool> m_trigDecTool {this, "TrigDecisionTool",""}; ///< Tool to tell whether a specific trigger is passed
+ private:  
+  /* properties */
+  Gaudi::Property<std::vector<std::string>> m_triggerList{
+      this, "triggerList", {}, "Add triggers to this to be monitored"};
+  Gaudi::Property<std::string> m_lutFile{this, "filepath_LUT", "TrigT1ZDC/zdcRun3T1LUT_v1_30_05_2023.json", "path to LUT file"};
+
+  /** A data member to hold the ZDCTrigger Object that stores input floats: shared ptr to ensure cleanup */
+  std::shared_ptr<ZDCTriggerSim::ModuleAmplInputsFloat> m_modInputs_p;
+
+  /** A data member to hold the ZDCTrigger Object that computes the LUT logic: shared ptr to ensure cleanup */
+  std::shared_ptr<ZDCTriggerSimModuleAmpls> m_simTrig;
+  
+  std::string m_msg;
+  std::map<std::string, unsigned int > m_triggerMap;
+  std::string m_auxSuffix;
+  std::string m_name;
+  bool m_writeAux;
+
+  
+};
+}
+#endif
diff --git a/ForwardDetectors/ZDC/ZdcTrigValid/src/components/ZdcTrigValid_entries.cxx b/ForwardDetectors/ZDC/ZdcTrigValid/src/components/ZdcTrigValid_entries.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..f73a37704ea49fb935337ee9ff773b82a6091d51
--- /dev/null
+++ b/ForwardDetectors/ZDC/ZdcTrigValid/src/components/ZdcTrigValid_entries.cxx
@@ -0,0 +1,7 @@
+#ifndef XAOD_STANDALONE
+
+#include "ZdcTrigValid/ZdcTrigValidTool.h"
+
+DECLARE_COMPONENT(ZDC::ZdcTrigValidTool)
+
+#endif
diff --git a/ForwardDetectors/ZDC/ZdcUtils/Root/ZDCTriggerSim.cxx b/ForwardDetectors/ZDC/ZdcUtils/Root/ZDCTriggerSim.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..51e45407fdaaccc8286bb1ea5bec4e872c3f5681
--- /dev/null
+++ b/ForwardDetectors/ZDC/ZdcUtils/Root/ZDCTriggerSim.cxx
@@ -0,0 +1,93 @@
+/*
+  Copyright (C) 2002-2023 CERN for the benefit of the ATLAS collaboration
+*/
+
+#include "ZdcUtils/ZDCTriggerSim.h"
+
+#include <iostream>
+#include <stdexcept>
+
+// dump stream data
+void ZDCTriggerSimBase::dump(std::ostream& strm) const {
+  for (auto entry : m_stack) {
+    strm << entry->getType() << ": ";
+    entry->dump(strm);
+    strm << std::endl;
+  }
+}
+
+// Obtain 3 bit output from 4*2 bit arm trigger decisions
+void ZDCTriggerSimCombLUT::doSimStage() {
+  ZDCTriggerSim::SimDataCPtr ptr = stackTopData();
+  if (ptr->getNumData() != 2 || ptr->getNumBits() != 4)
+    throw std::logic_error("Invalid input data in ZDCTriggerSimCombLUT");
+
+  unsigned int bitsSideA = ptr->getValueTrunc(0);
+  unsigned int bitsSideC = ptr->getValueTrunc(1);
+
+  unsigned int address = (bitsSideC << 4) + bitsSideA;
+  unsigned int comLUTvalue = m_combLUT.at(address);
+
+  // ZDCTriggerSim::SimDataPtr uses shared_ptr semantics so cleanup is
+  // guaranteed
+  //
+  ZDCTriggerSim::SimDataPtr lutOut_p(new ZDCTriggerSim::CombLUTOutput());
+  static_cast<ZDCTriggerSim::CombLUTOutput*>(lutOut_p.get())
+      ->setDatum(comLUTvalue);
+
+  stackPush(lutOut_p);
+}
+
+// Obtain 4x2 bit output from arm energy sums
+void ZDCTriggerSimAllLUTs::doSimStage() {
+  ZDCTriggerSim::SimDataCPtr ptr = stackTopData();
+  if (ptr->getNumData() != 2 || ptr->getNumBits() != 12)
+    throw std::logic_error("Invalid input data in ZDCTriggerSimAllLUTs");
+  ;
+
+  unsigned int inputSideA = ptr->getValueTrunc(0);
+  unsigned int inputSideC = ptr->getValueTrunc(1);
+
+  unsigned int valueA = m_LUTA.at(inputSideA);
+  unsigned int valueC = m_LUTC.at(inputSideC);
+
+  // ZDCTriggerSim::SimDataPtr uses shared_ptr semantics so cleanup is
+  // guaranteed
+  //
+  ZDCTriggerSim::SimDataPtr inputs_p(new ZDCTriggerSim::CombLUTInputsInt);
+  static_cast<ZDCTriggerSim::CombLUTInputsInt*>(inputs_p.get())
+      ->setData({valueA, valueC});
+
+  stackPush(ZDCTriggerSim::SimDataCPtr(inputs_p));
+  ZDCTriggerSimCombLUT::doSimStage();
+}
+
+// Obtain arm energy sums from Module by Module Calibrated energies
+void ZDCTriggerSimModuleAmpls::doSimStage() {
+  ZDCTriggerSim::SimDataCPtr ptr = stackTopData();
+  if (ptr->getNumData() != 8 || ptr->getNumBits() != 12)
+    throw std::logic_error("Invalid input data in ZDCTriggerSimModuleAmpls");
+
+  unsigned int sumA = 0;
+  for (size_t i = 0; i < 4; i++) {
+    sumA += ptr->getValueTrunc(i);
+  }
+
+  unsigned int sumC = 0;
+  for (size_t i = 4; i < 8; i++) {
+    sumC += ptr->getValueTrunc(i);
+  }
+
+  // The sums get divided by 4
+  //
+  sumA /= 4;
+  sumC /= 4;
+
+  ZDCTriggerSim::SimDataPtr inputs_p(new ZDCTriggerSim::SideLUTInputsInt);
+  static_cast<ZDCTriggerSim::SideLUTInputsInt*>(inputs_p.get())
+      ->setData({sumA, sumC});
+
+  stackPush(ZDCTriggerSim::SimDataCPtr(inputs_p));
+
+  ZDCTriggerSimAllLUTs::doSimStage();
+}
diff --git a/ForwardDetectors/ZDC/ZdcUtils/ZdcUtils/ZDCTriggerSim.h b/ForwardDetectors/ZDC/ZdcUtils/ZdcUtils/ZDCTriggerSim.h
new file mode 100644
index 0000000000000000000000000000000000000000..c8b6780570bc588523407bc16916d257563c28b3
--- /dev/null
+++ b/ForwardDetectors/ZDC/ZdcUtils/ZdcUtils/ZDCTriggerSim.h
@@ -0,0 +1,287 @@
+/*
+  Copyright (C) 2002-2023 CERN for the benefit of the ATLAS collaboration
+*/
+/**
+ * @file ZdcUtils/ZDCTriggerSim.h
+ * @author Brian Cole <bcole@cern.ch>, Matthew Hoppesch <mhoppesc@cern.ch>
+ * @date May 2023
+ * @brief A tool to make L1 decision using LUTs
+ */
+
+#ifndef ZDCTRIGVALID__ZDCTriggerSim__h
+#define ZDCTRIGVALID__ZDCTriggerSim__h
+
+#include <array>
+#include <cmath>
+#include <list>
+#include <memory>
+#include <ostream>
+#include <stdexcept>
+#include <type_traits>
+#include <vector>
+
+namespace ZDCTriggerSim {
+enum DataType { TCombLUTOutput, TCombLUTInput, TSideLUTsInput, TModAmplsInput };
+
+const std::vector<std::string> TypeStrings = {"CombLUTOutput", "CombLUTInput",
+                                              "SideLUTsInput", "ModAmplsInput"};
+}  // namespace ZDCTriggerSim
+
+//
+// Base class for simulation data. Primarily defines interface and virtual
+// destructor
+//
+class ZDCTriggerSimDataBase {
+ public:
+  virtual ~ZDCTriggerSimDataBase() = default;
+
+  virtual unsigned int getNumBits() const = 0;
+  virtual unsigned int getNumData() const = 0;
+  virtual ZDCTriggerSim::DataType getType() const = 0;
+
+  virtual unsigned int getValueTrunc(unsigned int idx = 0) const = 0;
+  virtual void dump(std::ostream& strm) const = 0;
+};
+
+// Template class that defines data of type T -- usually but not always unsigned
+// integer
+//   with NData different values and which will be truncated to NBits
+//
+//   The class also allows the use of conversion factors that are applied to the
+//   input values
+//     when the data is actually used. This allows translation between (e.g.)
+//     energies and ADC values or any other kind of required conversion.
+//
+//
+template <typename T, unsigned int NData, unsigned int NBits,
+          ZDCTriggerSim::DataType Type>
+class ZDCTriggerSimData : public ZDCTriggerSimDataBase {
+  bool m_doConvert;
+  std::vector<float> m_convertFactors;
+
+  std::vector<T> m_data;
+  bool m_haveData;
+
+  unsigned int doConvTrunc(const T& inValue) const {
+    unsigned int value = inValue;
+
+    if (m_doConvert) {
+      value = std::floor(inValue * m_convertFactors.at(0));
+    }
+
+    unsigned int valueTruncZero = std::max(static_cast<unsigned int>(0), value);
+    unsigned int valueTruncBits =
+        std::min(valueTruncZero, static_cast<unsigned int>((1 << NBits) - 1));
+    return valueTruncBits;
+  }
+
+  //  static const unsigned int maskBits = (2<<NBits) - 1;
+ public:
+  ZDCTriggerSimData()
+      : m_doConvert(false), m_data(NData, 0), m_haveData(false) {
+    static_assert(NData > 0, "ZDCTriggerSimData requires at least one datum");
+    static_assert(NBits > 0, "ZDCTriggerSimData requires at least 1 bit");
+  }
+
+  ZDCTriggerSimData(const std::vector<float>& conversionFactors)
+      : m_doConvert(true),
+        m_convertFactors(conversionFactors),
+        m_data(NData, 0),
+        m_haveData(false) {
+    static_assert(NData > 0, "ZDCTriggerSimData requires at least one datum");
+    static_assert(NBits > 0, "ZDCTriggerSimData requires at least 1 bit");
+  }
+
+  virtual ~ZDCTriggerSimData() override {}
+
+  unsigned int getNumBits() const override { return NBits; }
+  unsigned int getNumData() const override { return NData; }
+  virtual ZDCTriggerSim::DataType getType() const override { return Type; }
+
+  virtual unsigned int getValueTrunc(unsigned int idx = 0) const override {
+    if (!m_haveData)
+      throw std::logic_error("No data available for ZDCTriggerSimData");
+    return doConvTrunc(m_data.at(idx));
+  }
+
+  void setDatum(T datum) {
+    if (NData != 1)
+      throw std::logic_error(
+          "ZDCTriggerSimData setDatum called with NData > 1");
+    ;
+    m_haveData = true;
+    m_data[0] = datum;
+  }
+
+  void setData(const std::vector<T>& inData) {
+    m_data = inData;
+    m_haveData = true;
+  }
+
+  void clearData() { m_haveData = false; }
+
+  virtual void dump(std::ostream& strm) const override {
+    for (auto datum : m_data) {
+      strm << doConvTrunc(datum) << " ";
+    }
+  }
+};
+
+namespace ZDCTriggerSim {
+// The usual way we provide the module amplitudes
+//
+typedef ZDCTriggerSimData<unsigned int, 8, 12, TModAmplsInput>
+    ModuleAmplInputsInt;
+
+// The usual way we provide the module amplitudes
+//
+typedef ZDCTriggerSimData<float, 8, 12, TModAmplsInput> ModuleAmplInputsFloat;
+
+// The usual way we provide input to the side LUTs
+//
+typedef ZDCTriggerSimData<unsigned int, 2, 12, TSideLUTsInput> SideLUTInputsInt;
+
+// In case we want to be able to convert from energies or other floating input
+//
+typedef ZDCTriggerSimData<float, 2, 12, TSideLUTsInput> SideLUTInputsFloat;
+
+// The "usual" way we provide inputs to the combined LUT -- with unsigned
+// integers
+//
+typedef ZDCTriggerSimData<unsigned int, 2, 4, TCombLUTInput> CombLUTInputsInt;
+
+// In case we want to be able to convert from energies or other float
+//   to the integer inputs to the combined LUT
+//
+//  typedef ZDCTriggerSimData<float, 2, 4> CombLUTInputsFloat;
+
+// The combined LUT produces 3 output bits
+//
+typedef ZDCTriggerSimData<unsigned int, 1, 3, TCombLUTOutput> CombLUTOutput;
+
+typedef std::shared_ptr<const ZDCTriggerSimDataBase> SimDataCPtr;
+typedef std::shared_ptr<ZDCTriggerSimDataBase> SimDataPtr;
+}  // namespace ZDCTriggerSim
+
+// Base class for the ZDC trigger simulation.
+//
+// It is an abstract base that also provides the stack holding the intermediate
+// results
+//
+//
+class ZDCTriggerSimBase {
+ private:
+  typedef std::list<ZDCTriggerSim::SimDataCPtr> SimStack;
+
+  SimStack m_stack;
+
+ protected:
+  void stackClear() { m_stack.clear(); }
+
+  void stackPush(const ZDCTriggerSim::SimDataCPtr& ptr) {
+    m_stack.push_back(SimStack::value_type(ptr));
+  }
+
+  const ZDCTriggerSim::SimDataCPtr& stackTopData() const {
+    return m_stack.back();
+  }
+
+  // Take the data on the "top" of the stack and use it as input, adding new
+  // data to the stack
+  //
+  virtual void doSimStage() = 0;
+
+ public:
+  ZDCTriggerSimBase() = default;
+  virtual ~ZDCTriggerSimBase() = default;
+
+  // Every implementation of the base should ultimately produce the L1 bits
+  //   possibly (usually) through recursion
+  //
+  virtual unsigned int simLevel1Trig(
+      const ZDCTriggerSim::SimDataCPtr& data) = 0;
+
+  void dump(std::ostream& strm) const;
+};
+
+class ZDCTriggerSimCombLUT : virtual public ZDCTriggerSimBase {
+  std::array<unsigned int, 256> m_combLUT;
+
+ protected:
+  //
+  // The data on the top of the stack should be the two 4 bit inputs
+  //   to the combined LUT. The output is the combined LUT output
+  //
+  virtual void doSimStage() override;
+
+ public:
+  ZDCTriggerSimCombLUT(const std::array<unsigned int, 256>& inLUT)
+      : m_combLUT(inLUT) {}
+
+  virtual unsigned int simLevel1Trig(
+      const ZDCTriggerSim::SimDataCPtr& inputBits) override {
+    stackClear();
+    stackPush(inputBits);
+
+    doSimStage();
+    return stackTopData()->getValueTrunc();
+  }
+};
+
+class ZDCTriggerSimAllLUTs : virtual public ZDCTriggerSimBase,
+                             public ZDCTriggerSimCombLUT {
+  std::array<unsigned int, 4096> m_LUTA;
+  std::array<unsigned int, 4096> m_LUTC;
+
+ protected:
+  //
+  // The data on the top of the stack should be the two 12 bit inputs
+  //   to each of the side LUT. The output is the two side LUT outputs.
+  //
+  // After we excute the side LUT, we call the CombLUT doSimStage();
+  //
+  virtual void doSimStage() override;
+
+ public:
+  ZDCTriggerSimAllLUTs(const std::array<unsigned int, 4096>& sideALUT,
+                       const std::array<unsigned int, 4096>& sideCLUT,
+                       const std::array<unsigned int, 256>& inCombLUT)
+      : ZDCTriggerSimCombLUT(inCombLUT), m_LUTA(sideALUT), m_LUTC(sideCLUT) {}
+
+  virtual unsigned int simLevel1Trig(
+      const ZDCTriggerSim::SimDataCPtr& inputData) override {
+    stackClear();
+    stackPush(inputData);
+
+    doSimStage();
+    return stackTopData()->getValueTrunc();
+  }
+};
+
+class ZDCTriggerSimModuleAmpls : virtual public ZDCTriggerSimBase,
+                                 public ZDCTriggerSimAllLUTs {
+ protected:
+  //
+  // The data on the top of the stack should be the two 12 bit inputs
+  //   to each of the side LUT. The output is the two side LUT outputs.
+  //
+  // After we excute the side LUT, we call the CombLUT doSimStage();
+  //
+  virtual void doSimStage() override;
+
+ public:
+  ZDCTriggerSimModuleAmpls(const std::array<unsigned int, 4096>& sideALUT,
+                           const std::array<unsigned int, 4096>& sideCLUT,
+                           const std::array<unsigned int, 256>& inCombLUT)
+      : ZDCTriggerSimAllLUTs(sideALUT, sideCLUT, inCombLUT) {}
+
+  virtual unsigned int simLevel1Trig(
+      const ZDCTriggerSim::SimDataCPtr& inputData) override {
+    stackClear();
+    stackPush(inputData);
+
+    doSimStage();
+    return stackTopData()->getValueTrunc();
+  }
+};
+#endif
diff --git a/ForwardDetectors/ZDC/ZdcUtils/src/components/ZdcUtils_entries.cxx b/ForwardDetectors/ZDC/ZdcUtils/src/components/ZdcUtils_entries.cxx
index bb8b1bc02261bc26f3e20da37f647f3b506f1b93..d83d0bed9c79ccf65eb2d8eb796df44c876fbde1 100644
--- a/ForwardDetectors/ZDC/ZdcUtils/src/components/ZdcUtils_entries.cxx
+++ b/ForwardDetectors/ZDC/ZdcUtils/src/components/ZdcUtils_entries.cxx
@@ -1,4 +1,5 @@
 #include "ZdcUtils/ZDCWaveform.h"
 #include "ZdcUtils/ZDCWaveformFermiExp.h"
 #include "ZdcUtils/ZDCWaveformLTLinStep.h"
-#include "ZdcUtils/ZDCWaveformSampler.h"
\ No newline at end of file
+#include "ZdcUtils/ZDCWaveformSampler.h"
+#include "ZdcUtils/ZDCTriggerSim.h"