diff --git a/.gitignore b/.gitignore
index ea6d19ba7c20560a5fe7ccbb7c3aec2143ff4b7b..d8d55fee0fc12219f75f341b70fd505ea6544ec3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,4 +44,8 @@ calorimeters/test/
 # output files
 results/*
 
-*.sif
+# ROOT files
+*.root
+
+# local runtime files
+.local
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index e1a1d43619e8ec95475cbfd332fc3e42cc3cba12..58e58a0c83c56477665fd3f1ebe15c66c04d0799 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -9,7 +9,6 @@ default:
 stages:
   - generate
   - process
-  - analyze
   - collect
   - finish
 
diff --git a/.rootlogon.C b/.rootlogon.C
new file mode 100644
index 0000000000000000000000000000000000000000..0be78dd3599f186dcabb06c3c8d0c20177a8f509
--- /dev/null
+++ b/.rootlogon.C
@@ -0,0 +1,81 @@
+{
+  // Ensure fmt is loaded
+  R__LOAD_LIBRARY(libfmt);
+
+  // setup a local build directory so we don't polute our source code with
+  // ROOT dictionaries etc.
+  gSystem->SetBuildDir("/tmp/root_build");
+
+  // style definition based off the ATLAS style
+  TStyle* s = gStyle;
+
+  // use plain black on white colors
+  Int_t icol = 0; // WHITE
+  s->SetFrameBorderMode(icol);
+  s->SetFrameFillColor(icol);
+  s->SetCanvasBorderMode(icol);
+  s->SetCanvasColor(icol);
+  s->SetPadBorderMode(icol);
+  s->SetPadColor(icol);
+  s->SetStatColor(icol);
+  // s->SetFillColor(icol); // don't use: white fill color flor *all*
+  // objects
+
+  // set the paper & margin sizes
+  s->SetPaperSize(TStyle::kUSLetter);
+  s->SetPaperSize(20, 26);
+
+  // set margin sizes
+  s->SetPadTopMargin(0.05);
+  s->SetPadRightMargin(0.05);
+  s->SetPadBottomMargin(0.15);
+  s->SetPadLeftMargin(0.12);
+
+  // set title offsets (for axis label)
+  s->SetTitleXOffset(1.3);
+  s->SetTitleYOffset(1.1);
+
+  // use large fonts
+  // Int_t font=72; // Helvetica italics
+  Int_t font = 43; // Helvetica
+  Double_t tsize = 26;
+  s->SetTextFont(font);
+
+  s->SetTextSize(tsize);
+  s->SetLabelFont(font, "x");
+  s->SetTitleFont(font, "x");
+  s->SetLabelFont(font, "y");
+  s->SetTitleFont(font, "y");
+  s->SetLabelFont(font, "z");
+  s->SetTitleFont(font, "z");
+
+  s->SetLabelSize(tsize, "x");
+  s->SetTitleSize(tsize, "x");
+  s->SetLabelSize(tsize, "y");
+  s->SetTitleSize(tsize, "y");
+  s->SetLabelSize(tsize, "z");
+  s->SetTitleSize(tsize, "z");
+
+  // use bold lines and markers
+  s->SetMarkerStyle(20);
+  s->SetMarkerSize(1.2);
+  s->SetHistLineWidth(2.);
+  s->SetLineStyleString(2, "[12 12]"); // postscript dashes
+
+  // get rid of X error bars and y error bar caps
+  // s->SetErrorX(0.001);
+
+  // do not display any of the standard histogram decorations
+  s->SetOptTitle(0);
+  // s->SetOptStat(1111);
+  s->SetOptStat(0);
+  // s->SetOptFit(1111);
+  s->SetOptFit(0);
+
+  // put tick marks on top and RHS of plots
+  s->SetPadTickX(1);
+  s->SetPadTickY(1);
+
+  // lower amount of y-ticks
+  s->SetNdivisions(505, "Y");
+}
diff --git a/config/env.sh b/config/env.sh
index 0177dabc68f8e36e44746f8445adee9991735296..73aaf5981f656dfcd19cbf5fead712e88a35b27d 100755
--- a/config/env.sh
+++ b/config/env.sh
@@ -1,29 +1,80 @@
 #!/bin/bash
 
-if [[ ! -n  "${JUGGLER_DETECTOR}" ]] ; then 
+## =============================================================================
+## Global configuration variables for the benchmark scripts
+## The script defines the following environment variables that are meant to
+## be overriden by the Gitlab continuous integration (CI)
+##
+##  - JUGGLER_DETECTOR:       detector package to be used for the benchmark
+##  - JUGGLER_N_EVENTS:       #events processed by simulation/reconstruction
+##  - JUGGLER_INSTALL_PREFIX: location where Juggler (digi/recon) is installed
+##
+## It also defines the following additional variables for internal usage
+##  - LOCAL_PREFIX:           prefix for packages installed during the benchmark
+##  - DETECTOR_PREFIX:        prefix for the detector definitions
+##  - DETECTOR_PATH:          actual path with the detector definitions
+##
+## Finally, it makes sure LOCAL_PREFIX and JUGGLER_PREFIX are added to PATH
+## and LD_LIBRARY_PATH
+## =============================================================================
+
+echo "Setting up the Physics Benchmarks environment"
+
+## =============================================================================
+## Default variable definitions, normally these should be set
+## by the CI. In case of local development you may want to change these
+## in case you would like to modify the detector package or
+## number of events to be analyzed during the benchmark
+
+## Detector package to be used during the benchmark process
+if [ ! -n  "${JUGGLER_DETECTOR}" ] ; then 
   export JUGGLER_DETECTOR="topside"
 fi
+echo "JUGGLER_DETECTOR:       ${JUGGLER_DETECTOR}"
 
-if [[ ! -n  "${JUGGLER_N_EVENTS}" ]] ; then 
+## Number of events that will be processed by the reconstruction
+if [ ! -n  "${JUGGLER_N_EVENTS}" ] ; then 
   export JUGGLER_N_EVENTS=100
 fi
+echo "JUGGLER_N_EVENTS:       ${JUGGLER_N_EVENTS}"
 
-if [[ ! -n  "${JUGGLER_INSTALL_PREFIX}" ]] ; then 
+## Install prefix for juggler, needed to locate the Juggler xenv files.
+## Also used by the CI as install prefix for other packages where needed.
+## You should not have to touch this. Note that for local usage a different 
+## prefix structure is automatically used.
+if [ ! -n  "${JUGGLER_INSTALL_PREFIX}" ] ; then 
   export JUGGLER_INSTALL_PREFIX="/usr/local"
 fi
+## Ensure the juggler prefix is an absolute path
+export JUGGLER_INSTALL_PREFIX=`realpath ${JUGGLER_INSTALL_PREFIX}`
+echo "JUGGLER_INSTALL_PREFIX: ${JUGGLER_INSTALL_PREFIX}"
 
-# not sure this is needed
-if [[ ! -n "${DETECTOR_PREFIX}" ]]; then
-  # reuse the custom juggler install prefix for detector
-  export DETECTOR_INSTALL_PREFIX=${JUGGLER_INSTALL_PREFIX}
-fi
+## =============================================================================
+## Other utility variables that govern how some of the dependent packages
+## are built and installed. You should not have to change these.
 
-## ensure absolute paths
-# not sure this is needed either 
-export JUGGLER_INSTALL_PREFIX=`realpath ${JUGGLER_INSTALL_PREFIX}`
-export DETECTOR_INSTALL_PREFIX=`realpath ${DETECTOR_INSTALL_PREFIX}`
+## local prefix to be used for local storage of packages
+## downloaded/installed during the benchmark process
+LOCAL_PREFIX=".local"
+mkdir -p ${LOCAL_PREFIX}
+export LOCAL_PREFIX=`realpath ${LOCAL_PREFIX}`
+echo "LOCAL_PREFIX:           ${LOCAL_PREFIX}"
+
+## detector prefix: prefix for the detector definitions
+export DETECTOR_PREFIX="${LOCAL_PREFIX}/detector"
+mkdir -p ${DETECTOR_PREFIX}
+echo "DETECTOR_PREFIX:        ${DETECTOR_PREFIX}"
+
+## detector path: actual detector definition path
+export DETECTOR_PATH="${DETECTOR_PREFIX}/${JUGGLER_DETECTOR}"
+echo "DETECTOR_PATH:          ${DETECTOR_PATH}"
+
+## =============================================================================
+## Setup PATH and LD_LIBRARY_PATH to include our prefixes
+echo "Adding JUGGLER_INSTALL_PREFIX and LOCAL_PREFIX to PATH and LD_LIBRARY_PATH"
+export PATH=${JUGGLER_INSTALL_PREFIX}/bin:${LOCAL_PREFIX}/bin:${PATH}
+export LD_LIBRARY_PATH=${JUGGLER_INSTALL_PREFIX}/lib:${LOCAL_PREFIX}/lib:${LD_LIBRARY_PATH}
 
-## setup root results artifact path
-# this should be in the CI File instead
-# https://docs.gitlab.com/ee/ci/yaml/README.html#variables
-# export RESULTS_PATH=`realpath results`
+## =============================================================================
+## That's all!
+echo "Environment setup complete."
diff --git a/dis/config.yml b/dis/config.yml
index a300e5f034632f6d966711e166770c19696d9f07..c0dda0dc156b2ce3861891eb91dd3f9f694c6d22 100644
--- a/dis/config.yml
+++ b/dis/config.yml
@@ -1,5 +1,5 @@
 dis:run_test:
-  stage: analyze
+  stage: process
   timeout: 1 hours
   script:
     - bash dis/dis.sh
diff --git a/dvcs/config.yml b/dvcs/config.yml
index 9582ea62208316fd874bec5864122a4c4f6cc337..73cfc8e89e443ef17df3d92122c0859791726b33 100644
--- a/dvcs/config.yml
+++ b/dvcs/config.yml
@@ -7,15 +7,8 @@ dvcs:process:
     paths:
       - results
 
-dvcs:analysis:
-  stage: analyze
-  needs: ["dvcs:process"]
-  script:
-    - echo "THIS IS A PLACE HOLDER"
-
-
 dvcs:results:
   stage: collect
-  needs: ["dvcs:analysis"]
+  needs: ["dvcs:process"]
   script:
     - echo "All DVCS benchmarks successful"
diff --git a/dvmp/analysis/mt.h b/dvmp/analysis/mt.h
new file mode 100644
index 0000000000000000000000000000000000000000..198050c6ddc37e68518761b8ccc410e0d71ea123
--- /dev/null
+++ b/dvmp/analysis/mt.h
@@ -0,0 +1,11 @@
+#ifndef MT_H
+#define MT_H
+
+// Defines the number of threads to run within the ROOT analysis scripts.
+// TODO: make this a file configured by the CI scripts so we can specify
+//       the number of threads (and the number of processes) at a global
+//       level
+
+constexpr const int kNumThreads = 8;
+
+#endif
diff --git a/dvmp/analysis/plot.h b/dvmp/analysis/plot.h
new file mode 100644
index 0000000000000000000000000000000000000000..69ebba341af239541496a410c66f78eb33e8adac
--- /dev/null
+++ b/dvmp/analysis/plot.h
@@ -0,0 +1,40 @@
+#ifndef PLOT_H
+#define PLOT_H
+
+#include <TColor.h>
+#include <fmt/core.h>
+#include <vector>
+
+namespace plot {
+
+const int kMpBlue = TColor::GetColor(0x1f, 0x77, 0xb4);
+const int kMpOrange = TColor::GetColor(0xff, 0x7f, 0x0e);
+const int kMpGreen = TColor::GetColor(0x2c, 0xa0, 0x2c);
+const int kMpRed = TColor::GetColor(0xd6, 0x27, 0x28);
+const int kMpPurple = TColor::GetColor(0x94, 0x67, 0xbd);
+const int kMpBrown = TColor::GetColor(0x8c, 0x56, 0x4b);
+const int kMpPink = TColor::GetColor(0xe3, 0x77, 0xc2);
+const int kMpGrey = TColor::GetColor(0x7f, 0x7f, 0x7f);
+const int kMpMoss = TColor::GetColor(0xbc, 0xbd, 0x22);
+const int kMpCyan = TColor::GetColor(0x17, 0xbe, 0xcf);
+
+const std::vector<int> kPalette = {kMpBlue,   kMpOrange, kMpGreen, kMpRed,
+                                   kMpPurple, kMpBrown,  kMpPink,  kMpGrey,
+                                   kMpMoss,   kMpCyan};
+
+void draw_label(int ebeam, int pbeam, const std::string_view detector,
+                std::string_view vm, std::string_view what) {
+  auto t = new TPaveText(.15, 0.800, .7, .925, "NB NDC");
+  t->SetFillColorAlpha(kWhite, 0.4);
+  t->SetTextFont(43);
+  t->SetTextSize(25);
+  t->AddText(fmt::format("#bf{{{} }}SIMULATION", detector).c_str());
+  t->AddText(fmt::format("{} for {} DVMP.", what, vm).c_str());
+  t->AddText(fmt::format("{} GeV on {} GeV", ebeam, pbeam, what).c_str());
+  t->SetTextAlign(12);
+  t->Draw();
+}
+
+} // namespace plot
+
+#endif
diff --git a/dvmp/analysis/util.h b/dvmp/analysis/util.h
new file mode 100644
index 0000000000000000000000000000000000000000..874b70240c41323df0419a3b5276faae2e292054
--- /dev/null
+++ b/dvmp/analysis/util.h
@@ -0,0 +1,127 @@
+#ifndef UTIL_H
+#define UTIL_H
+
+#include <algorithm>
+#include <cmath>
+#include <exception>
+#include <fmt/core.h>
+#include <limits>
+#include <string>
+#include <vector>
+
+#include <Math/Vector4D.h>
+
+#include "dd4pod/Geant4ParticleCollection.h"
+#include "eicd/TrackParametersCollection.h"
+
+namespace util {
+
+// Exception definition for unknown particle errors
+// FIXME: A utility exception base class should be included in the analysis
+//        utility library, so we can skip most of this boilerplate
+class unknown_particle_error : public std::exception {
+public:
+  unknown_particle_error(std::string_view particle) : m_particle{particle} {}
+  virtual const char* what() const throw() {
+    return fmt::format("Unknown particle type: {}", m_particle).c_str();
+  }
+  virtual const char* type() const throw() { return "unknown_particle_error"; }
+
+private:
+  const std::string m_particle;
+};
+
+// Simple function to return the appropriate PDG mass for the particles
+// we care about for this process.
+// FIXME: consider something more robust (maybe based on hepPDT) to the
+//        analysis utility library
+inline double get_pdg_mass(std::string_view part) {
+  if (part == "electron") {
+    return 0.0005109989461;
+  } else if (part == "muon") {
+    return .1056583745;
+  } else if (part == "jpsi") {
+    return 3.0969;
+  } else if (part == "upsilon") {
+    return 9.49630;
+  } else {
+    throw unknown_particle_error{part};
+  }
+}
+
+// Get a vector of 4-momenta from raw tracking info, using an externally
+// provided particle mass assumption.
+// FIXME: should be part of utility library
+inline auto
+momenta_from_tracking(const std::vector<eic::TrackParametersData>& tracks,
+                      const double mass) {
+  std::vector<ROOT::Math::PxPyPzMVector> momenta{tracks.size()};
+  // transform our raw tracker info into proper 4-momenta
+  std::transform(tracks.begin(), tracks.end(), momenta.begin(),
+                 [mass](const auto& track) {
+                   // make sure we don't divide by zero
+                   if (fabs(track.qOverP) < 1e-9) {
+                     return ROOT::Math::PtEtaPhiMVector{};
+                   }
+                   const double pt = 1. / track.qOverP * sin(track.theta);
+                   const double eta = -log(tan(track.theta / 2));
+                   const double phi = track.phi;
+                   return ROOT::Math::PtEtaPhiMVector{pt, eta, phi, mass};
+                 });
+  return momenta;
+}
+
+// Get a vector of 4-momenta from the simulation data.
+// FIXME: should be part of utility library
+// TODO: Add PID selector (maybe using ranges?)
+inline auto
+momenta_from_simulation(const std::vector<dd4pod::Geant4ParticleData>& parts) {
+  std::vector<ROOT::Math::PxPyPzMVector> momenta{parts.size()};
+  // transform our simulation particle data into 4-momenta
+  std::transform(parts.begin(), parts.end(), momenta.begin(),
+                 [](const auto& part) {
+                   return ROOT::Math::PxPyPzMVector{part.psx, part.psy,
+                                                    part.psz, part.mass};
+                 });
+  return momenta;
+}
+
+// Find the decay pair candidates from a vector of particles (parts),
+// with invariant mass closest to a desired value (pdg_mass)
+// FIXME: not sure if this belongs here, or in the utility library. Probably the
+//        utility library
+inline std::pair<ROOT::Math::PxPyPzMVector, ROOT::Math::PxPyPzMVector>
+find_decay_pair(const std::vector<ROOT::Math::PxPyPzMVector>& parts,
+                const double pdg_mass) {
+  int first = -1;
+  int second = -1;
+  double best_mass = -1;
+
+  // go through all particle combinatorics, calculate the invariant mass
+  // for each combination, and remember which combination is the closest
+  // to the desired pdg_mass
+  for (int i = 0; i < parts.size(); ++i) {
+    for (int j = i + 1; j < parts.size(); ++j) {
+      const double new_mass{(parts[i] + parts[j]).mass()};
+      if (fabs(new_mass - pdg_mass) < fabs(best_mass - pdg_mass)) {
+        first = i;
+        second = j;
+        best_mass = new_mass;
+      }
+    }
+  }
+  if (first < 0) {
+    return {{}, {}};
+  }
+  return {parts[first], parts[second]};
+}
+// Calculate the invariant mass of a given pair of particles
+inline double
+get_im(const std::pair<ROOT::Math::PxPyPzMVector, ROOT::Math::PxPyPzMVector>&
+           particle_pair) {
+  return (particle_pair.first + particle_pair.second).mass();
+}
+
+} // namespace util
+
+#endif
diff --git a/dvmp/analysis/vm_mass.cxx b/dvmp/analysis/vm_mass.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..19c56db8957e780333f690e4033aee988a3a5a99
--- /dev/null
+++ b/dvmp/analysis/vm_mass.cxx
@@ -0,0 +1,108 @@
+#include "mt.h"
+#include "plot.h"
+#include "util.h"
+
+#include <ROOT/RDataFrame.hxx>
+#include <cmath>
+#include <fmt/color.h>
+#include <fmt/core.h>
+#include <iostream>
+#include <string>
+#include <vector>
+
+// Run VM invariant-mass-based benchmarks on an input reconstruction file for
+// a desired vector meson (e.g. jpsi) and a desired decay particle (e.g. muon)
+// Output figures are written to our output prefix (which includes the output
+// file prefix), and labeled with our detector name.
+// TODO: I think it would be better to pass small json configuration file to
+//       the test, instead of this ever-expanding list of function arguments.
+int vm_mass(std::string_view rec_file, std::string_view vm_name,
+            std::string_view decay_name, std::string_view detector,
+            std::string output_prefix) {
+  fmt::print(fmt::emphasis::bold | fg(fmt::color::forest_green),
+             "Running VM invariant mass analysis...\n");
+  fmt::print(" - Vector meson: {}\n", vm_name);
+  fmt::print(" - Decay particle: {}\n", decay_name);
+  fmt::print(" - Detector package: {}\n", detector);
+  fmt::print(" - output prefix: {}\n", output_prefix);
+
+  // Run this in multi-threaded mode if desired
+  ROOT::EnableImplicitMT(kNumThreads);
+
+  // The particles we are looking for. E.g. J/psi decaying into e+e-
+  const double vm_mass = util::get_pdg_mass(vm_name);
+  const double decay_mass = util::get_pdg_mass(decay_name);
+
+  // Ensure our output prefix always ends on a dot, a slash or a dash
+  if (output_prefix.back() != '.' && output_prefix.back() != '/' &&
+      output_prefix.back() != '-') {
+    output_prefix += "-";
+  }
+
+  // Open our input file file as a dataframe
+  ROOT::RDataFrame d{"events", rec_file};
+
+  // utility lambda functions to bind the vector meson and decay particle types
+  auto momenta_from_tracking =
+      [decay_mass](const std::vector<eic::TrackParametersData>& tracks) {
+        return util::momenta_from_tracking(tracks, decay_mass);
+      };
+  auto find_decay_pair =
+      [vm_mass](const std::vector<ROOT::Math::PxPyPzMVector>& parts) {
+        return util::find_decay_pair(parts, vm_mass);
+      };
+
+  // Define analysis flow
+  auto d_im =
+      d.Define("p_rec", momenta_from_tracking, {"outputTrackParameters"})
+          .Define("N", "p_rec.size()")
+          .Define("p_sim", util::momenta_from_simulation, {"mcparticles2"})
+          .Define("decay_pair_rec", find_decay_pair, {"p_rec"})
+          .Define("decay_pair_sim", find_decay_pair, {"p_sim"})
+          .Define("mass_rec", util::get_im, {"decay_pair_rec"})
+          .Define("mass_sim", util::get_im, {"decay_pair_sim"});
+
+  // Define output histograms
+  auto h_im_rec = d_im.Histo1D(
+      {"h_im_rec", ";m_{ll'} (GeV);#", 100, -1.1, vm_mass + 5}, "mass_rec");
+  auto h_im_sim = d_im.Histo1D(
+      {"h_im_sim", ";m_{ll'} (GeV);#", 100, -1.1, vm_mass + 5}, "mass_sim");
+
+  // Plot our histograms.
+  // TODO: to start I'm explicitly plotting the histograms, but want to
+  // factorize out the plotting code moving forward.
+  {
+    TCanvas c{"canvas", "canvas", 800, 800};
+    gPad->SetLogx(false);
+    gPad->SetLogy(false);
+    auto& h0 = *h_im_sim;
+    auto& h1 = *h_im_rec;
+    // histogram style
+    h0.SetLineColor(plot::kMpBlue);
+    h0.SetLineWidth(2);
+    h1.SetLineColor(plot::kMpOrange);
+    h1.SetLineWidth(2);
+    // axes
+    h0.GetXaxis()->CenterTitle();
+    h0.GetYaxis()->CenterTitle();
+    // draw everything
+    h0.DrawClone("hist");
+    h1.DrawClone("hist same");
+    // FIXME hardcoded beam configuration
+    plot::draw_label(10, 100, detector, vm_name, "Invariant mass");
+    TText* tptr;
+    auto t = new TPaveText(.6, .8417, .9, .925, "NB NDC");
+    t->SetFillColorAlpha(kWhite, 0);
+    t->SetTextFont(43);
+    t->SetTextSize(25);
+    tptr = t->AddText("simulated");
+    tptr->SetTextColor(plot::kMpBlue);
+    tptr = t->AddText("reconstructed");
+    tptr->SetTextColor(plot::kMpOrange);
+    t->Draw();
+    // Print canvas to output file
+    c.Print(fmt::format("{}vm_mass.png", output_prefix).c_str());
+  }
+  // That's all!
+  return 0;
+}
diff --git a/dvmp/config.yml b/dvmp/config.yml
index 65eb4ab823c1ddc1788b9749be4a366a1f244fd9..4a56b84ea109e93b058e9c5e2934e5e3f4f7c6bb 100644
--- a/dvmp/config.yml
+++ b/dvmp/config.yml
@@ -17,7 +17,7 @@ dvmp:generate:
   script:
     - ./dvmp/scripts/generate.sh --ebeam 10 --pbeam 100 --config jpsi_central --decay muon --decay electron
 
-dvmp:jpsi_central:process:
+dvmp:process:
   stage: process
   needs: ["dvmp:generate"]
   timeout: 1 hour
@@ -27,15 +27,8 @@ dvmp:jpsi_central:process:
     paths:
       - results
 
-dvmp:jpsi_central:test_analysis:
-  stage: analyze
-  needs: ["dvmp:jpsi_central:process"]
-  script:
-    - echo "THIS IS A PLACE HOLDER"
-
-
 dvmp:results:
   stage: collect
-  needs: ["dvmp:jpsi_central:test_analysis"]
+  needs: ["dvmp:process"]
   script:
     - echo "All DVMP benchmarks successful"
diff --git a/dvmp/dvmp.sh b/dvmp/dvmp.sh
index 7b7b2d9ae76f7952c1708c838f3e819e8b98b320..908f94ef814d2461a7dde86bdca4cffb1d0a4ab1 100644
--- a/dvmp/dvmp.sh
+++ b/dvmp/dvmp.sh
@@ -1,78 +1,104 @@
 #!/bin/bash
 
-if [[ ! -n  "${JUGGLER_DETECTOR}" ]] ; then 
-  export JUGGLER_DETECTOR="topside"
-fi
+## =============================================================================
+## Run the DVMP benchmarks in 5 steps:
+## 1. Build/install detector package
+## 2. Detector simulation through npsim
+## 3. Digitization and reconstruction through Juggler
+## 4. Root-based Physics analyses
+## 5. Finalize
+## =============================================================================
 
-if [[ ! -n  "${JUGGLER_N_EVENTS}" ]] ; then 
-  export JUGGLER_N_EVENTS=100
-fi
+echo "Running the DVMP benchmarks"
 
-# only used when running locally (not in CI)
-if [[ ! -n  "${JUGGLER_INSTALL_PREFIX}" ]] ; then 
-  export JUGGLER_INSTALL_PREFIX="/usr/local"
-fi
+## make sure we launch this script from the project root directory
+PROJECT_ROOT="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"/..
+pushd ${PROJECT_ROOT}
 
-# these variables might not need exported.
-export JUGGLER_FILE_NAME_TAG="dvmp"
-# Generator file
-export JUGGLER_GEN_FILE="results/dvmp/jpsi_central_electron-10on100-gen.hepmc"
-#export JUGGLER_GEN_FILE="${JUGGLER_FILE_NAME_TAG}.hepmc"
+## =============================================================================
+## Load the environment variables. To build the detector we need the following
+## variables:
+##
+## - JUGGLER_INSTALL_PREFIX: Install prefix for Juggler (simu/recon)
+## - JUGGLER_DETECTOR:       the detector package we want to use for this benchmark
+## - DETECTOR_PATH:          full path to the detector definitions
+##
+## You can ready config/env.sh for more in-depth explanations of the variables
+## and how they can be controlled.
+source config/env.sh
 
+## Extra environment variables for DVMP:
+## file tag for these tests
+JUGGLER_FILE_NAME_TAG="dvmp"
+# Generator file, hardcoded for now FIXME
+JUGGLER_GEN_FILE="results/dvmp/jpsi_central_electron-10on100-gen.hepmc"
+# FIXME use the input file name, as we will be generating a lot of these
+# in the future...
+## note: these variables need to be exported to be accessible from
+##       the juggler options.py. We should really work on a dedicated
+##       juggler launcher to get rid of these "magic" variables. FIXME
 export JUGGLER_SIM_FILE="sim_${JUGGLER_FILE_NAME_TAG}.root"
 export JUGGLER_REC_FILE="rec_${JUGGLER_FILE_NAME_TAG}.root"
 
-echo "JUGGLER_N_EVENTS      = ${JUGGLER_N_EVENTS}"
-echo "JUGGLER_DETECTOR      = ${JUGGLER_DETECTOR}"
-echo "JUGGLER_FILE_NAME_TAG = ${JUGGLER_FILE_NAME_TAG}"
-
-### Build the detector constructors.
-git clone https://eicweb.phy.anl.gov/EIC/detectors/${JUGGLER_DETECTOR}.git
-git clone https://eicweb.phy.anl.gov/EIC/detectors/accelerator.git
-pushd ${JUGGLER_DETECTOR}
-ln -s ../accelerator/eic
-popd
-mkdir ${JUGGLER_DETECTOR}/build
-pushd ${JUGGLER_DETECTOR}/build
-cmake ../. -DCMAKE_INSTALL_PREFIX=/usr/local && make -j30 install
-popd
 
-pushd ${JUGGLER_DETECTOR}
+## =============================================================================
+## Step 1: Build/install the desired detector package
+bash util/build_detector.sh
 
-## run geant4 simulations
+## =============================================================================
+## Step 2: Run the simulation
+echo "Running Geant4 simulation"
 npsim --runType batch \
       --part.minimalKineticEnergy 1000*GeV  \
       -v WARNING \
       --numberOfEvents ${JUGGLER_N_EVENTS} \
-      --compactFile ${JUGGLER_DETECTOR}.xml \
-      --inputFiles ../${JUGGLER_GEN_FILE} \
-      --outputFile  ${JUGGLER_SIM_FILE}
-if [[ "$?" -ne "0" ]] ; then
-  echo "ERROR running script"
+      --compactFile ${DETECTOR_PATH}/${JUGGLER_DETECTOR}.xml \
+      --inputFiles ${JUGGLER_GEN_FILE} \
+      --outputFile ${JUGGLER_SIM_FILE}
+if [ "$?" -ne "0" ] ; then
+  echo "ERROR running npsim"
   exit 1
 fi
 
-# Need to figure out how to pass file name to juggler from the commandline
+## =============================================================================
+## Step 3: Run digitization & reconstruction
+echo "Running the digitization and reconstruction"
+# FIXME Need to figure out how to pass file name to juggler from the commandline
 xenv -x ${JUGGLER_INSTALL_PREFIX}/Juggler.xenv \
-  gaudirun.py ../options/tracker_reconstruction.py
-if [[ "$?" -ne "0" ]] ; then
+  gaudirun.py options/tracker_reconstruction.py
+if [ "$?" -ne "0" ] ; then
   echo "ERROR running juggler"
   exit 1
 fi
 ls -l
-popd
 
-pwd
-mkdir -p results/dis
+## =============================================================================
+## Step 4: Analysis
+root -b -q "dvmp/analysis/vm_mass.cxx(\
+ \"${JUGGLER_REC_FILE}\", \
+ \"jpsi\", \
+ \"electron\", \
+ \"${JUGGLER_DETECTOR}\", \
+ \"results/dvmp/plot\")"
 
-echo "STAND-IN FOR ANALYSIS SCRIPT"
-#root -b -q "dis/scripts/rec_dis_electrons.cxx(\"${JUGGLER_DETECTOR}/${JUGGLER_REC_FILE}\")"
-#if [[ "$?" -ne "0" ]] ; then
-#  echo "ERROR running root script"
-#  exit 1
-#fi
+if [ "$?" -ne "0" ] ; then
+  echo "ERROR running root script"
+  exit 1
+fi
 
-if [[ "${JUGGLER_N_EVENTS}" -lt "500" ]] ; then 
-cp ${JUGGLER_DETECTOR}/${JUGGLER_REC_FILE} results/dvmp/.
+## =============================================================================
+## Step 5: finalize
+echo "Finalizing ${JUGGLER_FILE_NAME_TAG} benchmark"
+
+## Copy over reconsturction artifacts as long as we don't have
+## too many events
+if [ "${JUGGLER_N_EVENTS}" -lt "500" ] ; then 
+  cp ${JUGGLER_REC_FILE} results/dvmp/.
 fi
 
+## cleanup output files
+rm ${JUGGLER_REC_FILE} ${JUGGLER_SIM_FILE}
+
+## =============================================================================
+## All done!
+echo "${JUGGLER_FILE_NAME_TAG} benchmarks complete"
diff --git a/options/tracker_reconstruction.py b/options/tracker_reconstruction.py
index 75f104aee62a105069b4f2114548a48893761f6d..a9181a0a6beeeb464a341165bce4d1a8b16d8909 100644
--- a/options/tracker_reconstruction.py
+++ b/options/tracker_reconstruction.py
@@ -12,8 +12,10 @@ if "JUGGLER_DETECTOR" in os.environ :
 input_sim_file  = str(os.environ["JUGGLER_SIM_FILE"])
 output_rec_file = str(os.environ["JUGGLER_REC_FILE"])
 n_events = str(os.environ["JUGGLER_N_EVENTS"])
+detector_path = str(os.environ["DETECTOR_PATH"])
 
-geo_service  = GeoSvc("GeoSvc", detectors=["{}.xml".format(detector_name)])
+geo_service  = GeoSvc("GeoSvc",
+        detectors=["{}/{}.xml".format(detector_path, detector_name)])
 podioevent   = EICDataSvc("EventDataSvc", inputs=[input_sim_file], OutputLevel=DEBUG)
 
 from Configurables import PodioInput
diff --git a/util/build_detector.sh b/util/build_detector.sh
index 3d08a73c2dac69e04f9eddc2c82db258cf7abf55..066b11c2f1257286c0c282b59fb272866fc79d5d 100755
--- a/util/build_detector.sh
+++ b/util/build_detector.sh
@@ -1,28 +1,64 @@
 #!/bin/bash
 
-## Init the environment
+## =============================================================================
+## Build and install the JUGGLER_DETECTOR detector package into our local prefix
+## =============================================================================
+
+## make sure we launch this script from the project root directory
+PROJECT_ROOT="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"/..
+pushd ${PROJECT_ROOT}
+
+## =============================================================================
+## Load the environment variables. To build the detector we need the following
+## variables:
+##
+## - JUGGLER_DETECTOR: the detector package we want to use for this benchmark
+## - LOCAL_PREFIX:     location where local packages should be installed
+## - DETECTOR_PREFIX:  prefix for the detector definitions 
+## - DETECTOR_PATH:    full path for the detector definitions
+##                     this is the same as ${DETECTOR_PREFIX}/${JUGGLER_DETECTOR}
+##
+## You can ready config/env.sh for more in-depth explanations of the variables
+## and how they can be controlled.
 source config/env.sh
 
-## Build and install the detector plugins.
-if [[ ! -d ${JUGGLER_DETECTOR} ]]; then
+## =============================================================================
+## Step 1: download/update the detector definitions (if needed)
+pushd ${DETECTOR_PREFIX}
+
+## We need an up-to-date copy of the detector
+if [ ! -d ${JUGGLER_DETECTOR} ]; then
+  echo "Fetching ${JUGGLER_DETECTOR}"
   git clone https://eicweb.phy.anl.gov/EIC/detectors/${JUGGLER_DETECTOR}.git
-  # this might be temporary. There are multiple solutions here but this is the simple pattern for now
-  # I do not want to use git submodules here -whit
-  git clone https://eicweb.phy.anl.gov/EIC/detectors/accelerator.git
-  pushd ${JUGGLER_DETECTOR}
-  ln -s ../accelerator/eic
-  popd
 else
+  echo "Updating ${JUGGLER_DETECTOR}"
   pushd ${JUGGLER_DETECTOR}
   git pull --ff-only
   popd
+fi
+## We also need an up-to-date copy of the accelerator. For now this is done
+## manually. Down the road we could maybe automize this with cmake
+if [ ! -d accelerator ]; then
+  echo "Fetching accelerator"
+  git clone https://eicweb.phy.anl.gov/EIC/detectors/accelerator.git
+else
+  echo "Updating accelerator"
   pushd accelerator
   git pull --ff-only
   popd
 fi
-mkdir -p detector-build
-pushd detector-build
-# Always keep the detector directory at the top level.
-echo cmake ../${JUGGLER_DETECTOR} -DCMAKE_INSTALL_PREFIX=${DETECTOR_INSTALL_PREFIX} && make -j30 install
-cmake ../${JUGGLER_DETECTOR} -DCMAKE_INSTALL_PREFIX=${DETECTOR_INSTALL_PREFIX} && make -j30 install
-popd
+## Now symlink the accelerator definition into the detector definition
+echo "Linking accelerator definition into detector definition"
+ln -s -f ${DETECTOR_PREFIX}/accelerator/eic ${DETECTOR_PATH}/eic
+
+## =============================================================================
+## Step 2: Compile and install the detector definition
+echo "Building and installing the ${JUGGLER_DETECTOR} package"
+
+mkdir -p ${DETECTOR_PREFIX}/build
+pushd ${DETECTOR_PREFIX}/build
+cmake ${DETECTOR_PATH} -DCMAKE_INSTALL_PREFIX=${LOCAL_PREFIX} && make -j30 install
+
+## =============================================================================
+## Step 3: That's all!
+echo "Detector build/install complete!"
diff --git a/util/download_events.sh b/util/download.sh
similarity index 53%
rename from util/download_events.sh
rename to util/download.sh
index 3cf10fe626f43f31b1fde444c5f4153bb7be793c..47be238b461e3797bf124c46d21aaf33a348f8a5 100755
--- a/util/download_events.sh
+++ b/util/download.sh
@@ -1,18 +1,19 @@
 #!/bin/bash
 
-## Init the environment
-source config/env.sh
+## =============================================================================
+## Download generator & reconstruction artifacts for one or more physics
+## processes.
+## =============================================================================
 
-## Generates different configurations from the master configuration
-## for both electron and muon decay channels
-
-echo "Download generator artifacts for one or more of the physics processes"
+## make sure we launch this script from the project root directory
+PROJECT_ROOT="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"/..
+pushd ${PROJECT_ROOT}
 
 PROCS=()
-BRANCH="dvmp"
+BRANCH="master"
 
 function print_the_help {
-  echo "USAGE:    $0 [-c config [[-c config ...]] script1 [script2...]"
+  echo "USAGE:    -p process [-p process2] [-b git_branch]"
   echo "OPTIONS:"
   echo "          -p,--process  Physics process name (can be defined multiple
   times)."
@@ -25,7 +26,6 @@ function print_the_help {
   exit
 }
 
-
 while [ $# -gt 0 ]
 do
   key="$1"
@@ -35,7 +35,7 @@ do
       shift # past argument
       shift # past value
       ;;
-    --branch)
+    -b|--branch)
       BRANCH="$2"
       shift # past argument
       shift # past value
@@ -45,12 +45,14 @@ do
       shift
       ;;
     *)    # unknown option
-      echo "unknown option"
+      echo "unknown option: $1"
       exit 1
       ;;
   esac
 done
 
+echo "Downloading generator & reconstruction artifacts for one or more physics processes"
+
 if [ ${#PROCS[@]} -eq 0 ]; then
   echo "ERROR: need one or more processes: -p <process name> "
   exit 1
@@ -58,10 +60,14 @@ fi
 
 for proc in ${PROCS[@]}; do
   echo "Dowloading artifacts for $proc (branch: $BRANCH)"
-  wget https://eicweb.phy.anl.gov/EIC/benchmarks/physics_benchmarks/-/jobs/artifacts/$BRANCH/download?job=${proc}:jpsi_central:generate -O results.zip
+  wget https://eicweb.phy.anl.gov/EIC/benchmarks/physics_benchmarks/-/jobs/artifacts/$BRANCH/download?job=${proc}:generate -O results_gen.zip
+  ## FIXME this needs to be smarter, probably through more flags...
+  wget https://eicweb.phy.anl.gov/EIC/benchmarks/physics_benchmarks/-/jobs/artifacts/$BRANCH/download?job=${proc}:process -O results_rec.zip
   echo "Unpacking artifacts..."
-  unzip -u results.zip
+  unzip -u -o results_gen.zip
+  unzip -u -o results_rec.zip
   echo "Cleaning up..."
-  rm results.zip
+  rm results_???.zip
 done
+popd
 echo "All done"
diff --git a/util/start_dev_shell.sh b/util/start_dev_shell.sh
index 9741c2a924fe5e4562c909d21696bd6f27da25f4..d452525c4fce25ea3bb207a38cdc3b5e22ae9b14 100755
--- a/util/start_dev_shell.sh
+++ b/util/start_dev_shell.sh
@@ -1,21 +1,85 @@
 #!/bin/bash
 
+## =============================================================================
+## Setup (if needed) and start a development shell environment on Linux or MacOS
+## =============================================================================
+
+## make sure we launch this script from the project root directory
+PROJECT_ROOT="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"/..
+pushd ${PROJECT_ROOT}
+
+## We do not load the global development environment here, as this script is 
+## to be executed on a "naked" system outside of any container 
+
+## =============================================================================
+## Step 1: Parse command line options
+
+## do we want to force-update the container (only affects Linux)
+## default: we do not want to do this.
+FORCE_UPDATE=
+
+function print_the_help {
+  echo "USAGE:    ./util/start_dev_shell [-f]"
+  echo "OPTIONS:"
+  echo "          -f,--force    Force-update container (Only affects Linux)"
+  echo "          -h,--help     Print this message"
+  echo ""
+  echo "  This script will setup and launch a containerized development
+  environment"
+  exit
+}
+while [ $# -gt 0 ]
+do
+  key="$1"
+  case $key in
+    -f|--force)
+      FORCE_UPDATE="true"
+      shift # past value
+      ;;
+    -h|--help)
+      print_the_help
+      shift
+      ;;
+    *)    # unknown option
+      echo "unknown option $1"
+      exit 1
+      ;;
+  esac
+done
+
+## get OS type
 OS=`uname -s`
 
-if [ "${OS}" = "Linux" ]; then
-  echo "Detected OS: Linux"
-  if [ ! -f juggler_latest.sif ]; then
-    echo "Need to fetch singularity image"
-    wget https://eicweb.phy.anl.gov/eic/juggler/-/jobs/artifacts/master/raw/build/juggler.sif?job=docker:singularity -O juggler_latest.sif
-  fi
-  echo "Launching dev shell (through singularity)..."
-  singularity exec juggler_latest.sif eic-shell
-elif [ "${OS}" = "Darwin" ]; then
-  echo "Detector OS: MacOS"
-  echo "Syncing docker container"
-  docker pull sly2j/juggler:latest
-  echo "Launching dev shell (through docker)..."
-  docker run -v /Users:/Users -w=$PWD -i -t --rm sly2j/juggler:latest eic-shell
-else
-  echo "ERROR: dev shell not available for this OS (${OS})"
-fi
+## =============================================================================
+## Step 2: Update container and launch shell
+echo "Launching a containerized development shell"
+
+case ${OS} in
+  Linux)
+    echo "  - Detected OS: Linux"
+    ## Use the same prefix as we use for other local packages
+    export PREFIX=.local/lib
+    if [ ! -f $PREFIX/juggler_latest.sif ] || [ ! -z ${FORCE_UPDATE} ]; then
+      echo "  - Fetching singularity image"
+      mkdir -p $PREFIX
+      wget https://eicweb.phy.anl.gov/eic/juggler/-/jobs/artifacts/master/raw/build/juggler.sif?job=docker:singularity
+      -O $PREFIX/juggler_latest.sif
+    fi
+    echo "  - Using singularity to launch shell..."
+    singularity exec $PREFIX/juggler_latest.sif eic-shell
+    ;;
+  Darwin)
+    echo "  - Detector OS: MacOS"
+    echo "  - Syncing docker container"
+    docker pull sly2j/juggler:latest
+    echo "  - Using docker to launch shell..."
+    docker run -v /Users:/Users -w=$PWD -i -t --rm sly2j/juggler:latest eic-shell
+    ;;
+  *)
+    echo "ERROR: dev shell not available for this OS (${OS})"
+    exit 1
+esac
+
+## =============================================================================
+## Step 3: All done
+echo "Exiting development environment..."