diff --git a/part3/cmssw/src/L1Trigger/L1TMLDemo/plugins/BuildFile.xml b/part3/cmssw/src/L1Trigger/L1TMLDemo/plugins/BuildFile.xml
index 7431fa5b0316d88b776477fb47f8e34022eff6a9..823a0601dab6b4d6babc83252cae28accc8b0db9 100644
--- a/part3/cmssw/src/L1Trigger/L1TMLDemo/plugins/BuildFile.xml
+++ b/part3/cmssw/src/L1Trigger/L1TMLDemo/plugins/BuildFile.xml
@@ -3,6 +3,7 @@
 <use name="FWCore/PluginManager"/>
 <use name="DataFormats/L1Trigger"/>
 <use name="DataFormats/NanoAOD"/>
+<use name="L1Trigger/DemonstratorTools"/>
 <use name="hls"/>
 <use name="hls4mlEmulatorExtras"/>
 <flags EDM_PLUGIN="1"/>
\ No newline at end of file
diff --git a/part3/cmssw/src/L1Trigger/L1TMLDemo/plugins/l1tDemoMLPatternWriter.cc b/part3/cmssw/src/L1Trigger/L1TMLDemo/plugins/l1tDemoMLPatternWriter.cc
new file mode 100644
index 0000000000000000000000000000000000000000..7e3fc49255a5fe185c4b8f00c581b4e503889382
--- /dev/null
+++ b/part3/cmssw/src/L1Trigger/L1TMLDemo/plugins/l1tDemoMLPatternWriter.cc
@@ -0,0 +1,215 @@
+// FWCore includes
+#include "FWCore/Framework/interface/one/EDAnalyzer.h"
+#include "FWCore/Framework/interface/Event.h"
+#include "FWCore/Framework/interface/MakerMacros.h"
+#include "FWCore/Framework/interface/ESHandle.h"
+#include "FWCore/Framework/interface/EventSetup.h"
+#include "FWCore/ParameterSet/interface/ParameterSet.h"
+
+// File writing includes
+#include "DataFormats/NanoAOD/interface/FlatTable.h"
+
+// L1T includes
+#include "DataFormats/L1Trigger/interface/Muon.h"
+#include "DataFormats/L1Trigger/interface/EGamma.h"
+#include "DataFormats/L1Trigger/interface/Tau.h"
+#include "DataFormats/L1Trigger/interface/Jet.h"
+#include "DataFormats/L1Trigger/interface/EtSum.h"
+
+#include "L1Trigger/DemonstratorTools/interface/BoardDataWriter.h"
+
+// hls & hls4ml includes
+#include "ap_fixed.h"
+#include "hls4ml/emulator.h"
+
+#include <iostream>
+
+class L1TMLDemoPatternWriter : public edm::one::EDAnalyzer<edm::one::SharedResources> {
+public:
+  explicit L1TMLDemoPatternWriter(const edm::ParameterSet& cfg);
+  ~L1TMLDemoPatternWriter();
+
+private:
+  virtual void beginJob() override;
+  virtual void analyze(const edm::Event&, const edm::EventSetup&);
+  virtual void endJob() override;
+  void pack_inputs(ap_fixed<14,13>* X_unscaled);
+  void pack_outputs(ap_fixed<13,2,AP_RND,AP_SAT> y);
+
+  edm::EDGetToken muToken;
+  edm::EDGetToken egToken;
+  edm::EDGetToken tauToken;
+  edm::EDGetToken jetToken;
+  edm::EDGetToken sumToken;
+
+  unsigned nMu;
+  unsigned nEG;
+  unsigned nTau;
+  unsigned nJet;
+  unsigned nNNIn;
+
+  typedef ap_fixed<16,6,AP_RND_CONV,AP_SAT> scale_t;
+  typedef ap_fixed<16,6,AP_RND_CONV,AP_SAT> bias_t;
+  // hls4ml emulator model path
+  std::string model_so_path;
+  std::vector<scale_t> scale;
+  std::vector<bias_t> bias;
+
+  bool write_patterns;
+  //l1t::demo::BoardDataWriter inFileWriter;
+  std::map<l1t::demo::LinkId, std::pair<l1t::demo::ChannelSpec, std::vector<size_t>>> outChannelSpec;
+
+  l1t::demo::BoardDataWriter outFileWriter;
+
+};
+
+L1TMLDemoPatternWriter::L1TMLDemoPatternWriter(const edm::ParameterSet& cfg)
+ : outChannelSpec{{{"y", 0}, {{1,0}, {0}}}}, 
+   outFileWriter(l1t::demo::FileFormat::EMPv2,        // pattern file format
+                 "L1TMLDemoPatterns_out",             // file name
+                 "txt",                               // file extension
+                 1,                                   // frames per BX
+                 1,                                   // TMUX 
+                 1024,                                // max lines per file
+                 outChannelSpec)
+ {
+  // consume
+  muToken = consumes<l1t::MuonBxCollection>(cfg.getParameter<edm::InputTag>("muToken"));
+  egToken = consumes<l1t::EGammaBxCollection>(cfg.getParameter<edm::InputTag>("egToken"));
+  tauToken = consumes<l1t::TauBxCollection>(cfg.getParameter<edm::InputTag>("tauToken"));
+  jetToken = consumes<l1t::JetBxCollection>(cfg.getParameter<edm::InputTag>("jetToken"));
+  sumToken = consumes<l1t::EtSumBxCollection>(cfg.getParameter<edm::InputTag>("etSumToken"));
+  nMu = cfg.getParameter<unsigned>("nMu");
+  nEG = cfg.getParameter<unsigned>("nEg");
+  nTau = cfg.getParameter<unsigned>("nTau");
+  nJet = cfg.getParameter<unsigned>("nJet");
+  // total number of inputs to NN
+  nNNIn = 2 + 3 * (nMu + nEG + nTau + nJet);
+
+  // store the path to the .so file
+  model_so_path = cfg.getParameter<std::string>("model_so_path");
+
+  // get the scaler parameters and cast them to fixed point types
+  std::vector<double> scale_double = cfg.getParameter<std::vector<double>>("scale");
+  std::transform(scale_double.begin(), scale_double.end(), std::back_inserter(scale), [](double s){ return (scale_t)s; });
+  // get the bias parameters and cast them to fixed point types
+  std::vector<double> bias_double = cfg.getParameter<std::vector<double>>("bias");
+  std::transform(bias_double.begin(), bias_double.end(), std::back_inserter(bias), [](double s){ return (bias_t)s; });
+
+  // configure the board writer
+  write_patterns = cfg.getParameter<bool>("write_patterns");
+                                       
+}
+
+L1TMLDemoPatternWriter::~L1TMLDemoPatternWriter(){
+}
+
+void L1TMLDemoPatternWriter::analyze(const edm::Event& iEvent, const edm::EventSetup& iSetup)  {
+  using namespace edm;
+  // get input collections
+  // BXVector: first index is BX, second index is object
+  edm::Handle<BXVector<l1t::Muon>> muons;
+  edm::Handle<BXVector<l1t::EGamma>> egammas;
+  edm::Handle<BXVector<l1t::Tau>> taus;
+  edm::Handle<BXVector<l1t::Jet>> jets;
+  edm::Handle<BXVector<l1t::EtSum>> sums;
+  iEvent.getByToken(muToken, muons);
+  iEvent.getByToken(egToken, egammas);
+  iEvent.getByToken(tauToken, taus);
+  iEvent.getByToken(jetToken, jets);
+  iEvent.getByToken(sumToken, sums);
+
+  // The unscaled inputs are hwInts
+  // ap_fixed<14,13> is wide enough for all the ET, pT, eta, phi
+  ap_fixed<14,13>* X_unscaled = new ap_fixed<14,13>[nNNIn];
+  // initialize to zeros
+  for(unsigned i = 0; i < nNNIn; i++){
+    X_unscaled[i] = 0;
+  }
+
+  // fill the inputs
+  unsigned ix = 0;
+  // sums first, just find the MET
+  for(unsigned i = 0; i < sums->size(0); i++){
+    if(sums->at(0, i).getType() == l1t::EtSum::EtSumType::kMissingEt){
+      X_unscaled[ix++] = sums->at(0,i).hwPt();
+      X_unscaled[ix++] = sums->at(0,i).hwPhi();
+    }
+  }
+  // jets next
+  ix = 2 * ( 1 );
+  for(unsigned i = 0; i < std::min(nJet, jets->size(0)); i++){
+    X_unscaled[ix++] = jets->at(0, i).hwPt();
+    X_unscaled[ix++] = jets->at(0, i).hwEta();
+    X_unscaled[ix++] = jets->at(0, i).hwPhi();
+  }
+  // egammas next
+  ix = 2 * ( 1 + nJet );
+  for(unsigned i = 0; i < std::min(nEG, egammas->size(0)); i++){
+    X_unscaled[ix++] = egammas->at(0, i).hwPt();
+    X_unscaled[ix++] = egammas->at(0, i).hwEta();
+    X_unscaled[ix++] = egammas->at(0, i).hwPhi();
+  }
+  // muons next
+  ix = 2 * ( 1 + nJet + nEG );
+  for(unsigned i = 0; i < std::min(nMu, muons->size(0)); i++){
+    X_unscaled[ix++] = muons->at(0, i).hwPt();
+    X_unscaled[ix++] = muons->at(0, i).hwEta();
+    X_unscaled[ix++] = muons->at(0, i).hwPhi();
+  }
+  // taus next
+  ix = 2 * ( 1 + nJet + nEG + nMu );
+  for(unsigned i = 0; i < std::min(nTau, taus->size(0)); i++){
+    X_unscaled[ix++] = taus->at(0, i).hwPt();
+    X_unscaled[ix++] = taus->at(0, i).hwEta();
+    X_unscaled[ix++] = taus->at(0, i).hwPhi();
+  }
+
+  ap_fixed<16,7,AP_RND,AP_SAT>* X_scaled = new ap_fixed<16,7,AP_RND,AP_SAT>[nNNIn];
+  // scale the inputs
+  for(unsigned i = 0; i < nNNIn; i++){
+    X_scaled[i] = (X_unscaled[i] - bias[i]) * scale[i];
+    //std::cout << X_scaled[i] << ",";
+  }
+  //std::cout << std::endl;
+
+  // load the NN emulator object
+  hls4mlEmulator::ModelLoader loader(model_so_path);
+  std::shared_ptr<hls4mlEmulator::Model> model = loader.load_model();
+
+  ap_fixed<13,2,AP_RND,AP_SAT> y; // output object
+
+  // run the actual inference
+  model->prepare_input(X_scaled);
+  model->predict();
+  model->read_result(&y);
+
+  // write the patterns
+  if(write_patterns){
+    pack_outputs(y);
+  }
+
+}
+
+void L1TMLDemoPatternWriter::beginJob(){
+}
+
+void L1TMLDemoPatternWriter::endJob(){
+  outFileWriter.flush();
+}
+
+void L1TMLDemoPatternWriter::pack_inputs(ap_fixed<14,13>* X_unscaled) {
+
+}
+
+void L1TMLDemoPatternWriter::pack_outputs(ap_fixed<13,2,AP_RND,AP_SAT> y) {
+  ap_uint<64> bits = 0;
+  bits(12,0) = y(12,0); // copy the bits not the value
+  std::vector<ap_uint<64>> bits_v;
+  bits_v.push_back(bits);
+  l1t::demo::EventData eventDataOut;
+  eventDataOut.add({"y", 0}, bits_v);
+  outFileWriter.addEvent(eventDataOut);
+}
+
+DEFINE_FWK_MODULE(L1TMLDemoPatternWriter);
diff --git a/part3/cmssw/src/L1Trigger/L1TMLDemo/plugins/l1tDemoMLAnalyzer.cc b/part3/cmssw/src/L1Trigger/L1TMLDemo/plugins/l1tDemoMLProducer.cc
similarity index 100%
rename from part3/cmssw/src/L1Trigger/L1TMLDemo/plugins/l1tDemoMLAnalyzer.cc
rename to part3/cmssw/src/L1Trigger/L1TMLDemo/plugins/l1tDemoMLProducer.cc
diff --git a/part3/cmssw/src/L1Trigger/L1TMLDemo/test/demoL1TMLNtuple.py b/part3/cmssw/src/L1Trigger/L1TMLDemo/test/demoL1TMLNtuple.py
index dc4ae28d3f6bde85c28c54c0dff06064fc49c479..3070d51c8820153a3b64001ecf5a4b00e76f5609 100644
--- a/part3/cmssw/src/L1Trigger/L1TMLDemo/test/demoL1TMLNtuple.py
+++ b/part3/cmssw/src/L1Trigger/L1TMLDemo/test/demoL1TMLNtuple.py
@@ -11,7 +11,7 @@ import FWCore.ParameterSet.Config as cms
 # note you should not really load these from a pkl file for real CMSSW
 import os
 import pickle
-scales_file = os.environ['MLATL1T_DIR'] + '/part1_outputs/hwScaler.pkl'
+scales_file = os.environ['MLATL1T_DIR'] + '/part1/part1_outputs/hwScaler.pkl'
 scales = pickle.load(open(scales_file, 'rb'))
 # the standard scaler does (x - u) / s while we will do (x - u) * (1 / s) so invert s here
 scale = 1. / scales.scale_
@@ -28,8 +28,9 @@ process.load('Configuration.StandardSequences.SimL1Emulator_cff')
 process.load('Configuration.StandardSequences.EndOfProcess_cff')
 process.load('Configuration.StandardSequences.FrontierConditions_GlobalTag_cff')
 
+NEvents = options.maxEvents
 process.maxEvents = cms.untracked.PSet(
-    input = cms.untracked.int32(100_000)
+    input = cms.untracked.int32(NEvents)
 )
 
 filelist = 'files_signal.txt' if options.signal else 'files_background.txt'
@@ -43,6 +44,8 @@ process.source = cms.Source (
 from Configuration.AlCa.GlobalTag import GlobalTag
 process.GlobalTag = GlobalTag(process.GlobalTag, 'auto:startup', '')
 
+# only write pattern files if running on 1000 events or fewer
+write_patterns = NEvents <= 1000
 process.l1tDemoMLProducer = cms.EDProducer('L1TMLDemoProducer',
     muToken    = cms.InputTag("gmtStage2Digis:Muon"),
     egToken    = cms.InputTag("caloStage2Digis:EGamma"),
@@ -58,11 +61,32 @@ process.l1tDemoMLProducer = cms.EDProducer('L1TMLDemoProducer',
     bias = cms.vdouble(*bias),
 )
 
+write_patterns = NEvents <= 1000
+process.l1tDemoMLPatternWriter = cms.EDAnalyzer('L1TMLDemoPatternWriter',
+    muToken    = cms.InputTag("gmtStage2Digis:Muon"),
+    egToken    = cms.InputTag("caloStage2Digis:EGamma"),
+    tauToken   = cms.InputTag("caloStage2Digis:Tau"),
+    jetToken   = cms.InputTag("caloStage2Digis:Jet"),
+    etSumToken = cms.InputTag("caloStage2Digis:EtSum"),
+    nMu = cms.uint32(2),
+    nEg = cms.uint32(8),
+    nTau = cms.uint32(0),
+    nJet = cms.uint32(8),
+    model_so_path = cms.string("../data/L1TMLDemo_v1"),
+    scale = cms.vdouble(*scale),
+    bias = cms.vdouble(*bias),
+    write_patterns = cms.bool(write_patterns),
+)
+
 process.path = cms.Path(
-    process.l1tDemoMLProducer
+    process.l1tDemoMLProducer + 
+    process.l1tDemoMLPatternWriter
 )
 
-oname = 'L1TMLDemo_NanoAOD_signal.root' if options.signal else 'L1TMLDemo_NanoAOD_background.root'
+
+signal_ext = '_signal' if options.signal else '_backgroud'
+writer_ext = '_patterns' if write_patterns else ''
+oname = 'L1TMLDemo_NanoAOD' + signal_ext + writer_ext + '.root'
 process.outnano = cms.OutputModule("NanoAODOutputModule",
     fileName = cms.untracked.string(oname),
     outputCommands = cms.untracked.vstring("drop *", "keep nanoaodFlatTable_*_*_*"),
diff --git a/part3/emulator_exercise.md b/part3/emulator_exercise.md
index 234f35e837f3e5916143c860433650ddaad11914..c70aa903b0436bc51f1177d12c3c25511d57a30e 100644
--- a/part3/emulator_exercise.md
+++ b/part3/emulator_exercise.md
@@ -75,8 +75,9 @@ Run the test config over signal and background!
 
 ```shell
 cd $CMSSW_BASE/src/L1Trigger/L1TMLDemo/test
-cmsRun demoL1TMLNtuple.py signal=True
-cmsRun demoL1TMLNtuple.py signal=False
+cmsRun demoL1TMLNtuple.py maxEvents=100000 signal=True
+cmsRun demoL1TMLNtuple.py maxEvents=100000 signal=False
+cmsRun demoL1TMLNtuple.py maxEvents=1000 signal=True # produce a pattern file for the hardware test
 ```
 
 We run over the same datasets as part 1: