From a0be437a4045201d08d85e9bed17eeaa14ba6dbc Mon Sep 17 00:00:00 2001
From: Sylvester Joosten <sylvester.joosten@gmail.com>
Date: Mon, 21 Dec 2020 22:56:00 -0600
Subject: [PATCH] Add benchmark/exception headers as symlink to the benchmarks
 for now, in the future these should be moved to the util library

---
 dvmp/analysis/benchmark.hh |   1 +
 dvmp/analysis/exception.hh |   1 +
 dvmp/analysis/vm_mass.cxx  |  43 ++++++++++++--
 dvmp/dvmp.sh               |  26 ++++++---
 dvmp/env.sh                |  10 +++-
 util/benchmark.hh          | 114 +++++++++++++++++++++++++++++++++++++
 util/exception.hh          |  23 ++++++++
 util/run_many.py           |   8 ++-
 8 files changed, 210 insertions(+), 16 deletions(-)
 create mode 120000 dvmp/analysis/benchmark.hh
 create mode 120000 dvmp/analysis/exception.hh
 create mode 100644 util/benchmark.hh
 create mode 100644 util/exception.hh

diff --git a/dvmp/analysis/benchmark.hh b/dvmp/analysis/benchmark.hh
new file mode 120000
index 00000000..a9677242
--- /dev/null
+++ b/dvmp/analysis/benchmark.hh
@@ -0,0 +1 @@
+../../util/benchmark.hh
\ No newline at end of file
diff --git a/dvmp/analysis/exception.hh b/dvmp/analysis/exception.hh
new file mode 120000
index 00000000..e50e23b6
--- /dev/null
+++ b/dvmp/analysis/exception.hh
@@ -0,0 +1 @@
+../../util/exception.hh
\ No newline at end of file
diff --git a/dvmp/analysis/vm_mass.cxx b/dvmp/analysis/vm_mass.cxx
index 19c56db8..26d0cf9a 100644
--- a/dvmp/analysis/vm_mass.cxx
+++ b/dvmp/analysis/vm_mass.cxx
@@ -1,3 +1,4 @@
+#include "benchmark.hh"
 #include "mt.h"
 #include "plot.h"
 #include "util.h"
@@ -6,7 +7,9 @@
 #include <cmath>
 #include <fmt/color.h>
 #include <fmt/core.h>
+#include <fstream>
 #include <iostream>
+#include <nlohmann/json.hpp>
 #include <string>
 #include <vector>
 
@@ -16,9 +19,19 @@
 // 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) {
+int vm_mass(const std::string& config_name) {
+  // read our configuration
+  std::ifstream config_file{config_name};
+  nlohmann::json config;
+  config_file >> config;
+
+  const std::string rec_file = config["rec_file"];
+  const std::string vm_name = config["vm_name"];
+  const std::string decay_name = config["decay"];
+  const std::string detector = config["detector"];
+  std::string output_prefix = config["output_prefix"];
+  const std::string test_tag = config["test_tag"];
+
   fmt::print(fmt::emphasis::bold | fg(fmt::color::forest_green),
              "Running VM invariant mass analysis...\n");
   fmt::print(" - Vector meson: {}\n", vm_name);
@@ -26,6 +39,18 @@ int vm_mass(std::string_view rec_file, std::string_view vm_name,
   fmt::print(" - Detector package: {}\n", detector);
   fmt::print(" - output prefix: {}\n", output_prefix);
 
+  // create our test definition
+  // test_tag
+  juggler_util::test vm_mass_resolution_test{
+      {{"name",
+        fmt::format("{}_{}_{}_mass_resolution", test_tag, vm_name, decay_name)},
+       {"title",
+        fmt::format("{} -> {} Invariant Mass Resolution", vm_name, decay_name)},
+       {"description", "Invariant Mass Resolution calculated from raw "
+                       "tracking data using a Gaussian fit."},
+       {"quantity", "resolution"},
+       {"target", ".1"}}};
+
   // Run this in multi-threaded mode if desired
   ROOT::EnableImplicitMT(kNumThreads);
 
@@ -42,7 +67,8 @@ int vm_mass(std::string_view rec_file, std::string_view vm_name,
   // 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
+  // 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);
@@ -103,6 +129,15 @@ int vm_mass(std::string_view rec_file, std::string_view vm_name,
     // Print canvas to output file
     c.Print(fmt::format("{}vm_mass.png", output_prefix).c_str());
   }
+
+  // TODO we're not actually doing an IM fit yet, so for now just return an
+  // error for the test result
+  vm_mass_resolution_test.error(-1);
+
+  // write out our test data
+  juggler_util::write_test(vm_mass_resolution_test,
+                           fmt::format("{}vm_mass.json", output_prefix));
+
   // That's all!
   return 0;
 }
diff --git a/dvmp/dvmp.sh b/dvmp/dvmp.sh
index 6d4e1844..acad1d1c 100755
--- a/dvmp/dvmp.sh
+++ b/dvmp/dvmp.sh
@@ -42,6 +42,7 @@ source config/env.sh
 ## We also need the following benchmark-specific variables:
 ##
 ## - BENCHMARK_TAG: Unique identified for this benchmark process.
+## - BEAM_TAG:      Identifier for the chosen beam configuration
 ## - INPUT_PATH:    Path for generator-level input to the benchmarks
 ## - TMP_PATH:      Path for temporary data (not exported as artifacts)
 ## - RESULTS_PATH:  Path for benchmark output figures and files
@@ -105,16 +106,27 @@ if [ "$?" -ne "0" ] ; then
     exit 1
   fi
 fi
-ls -l
+#ls -l
 
 ## =============================================================================
 ## Step 4: Analysis
-root -b -q "dvmp/analysis/vm_mass.cxx(\
- \"${REC_FILE}\", \
- \"${LEADING}\", \
- \"${DECAY}\", \
- \"${JUGGLER_DETECTOR}\", \
- \"${RESULTS_PATH}/${PLOT_TAG}\")"
+
+## write a temporary configuration file for the analysis script
+CONFIG="${TMP_PATH}/${PLOT_TAG}.json"
+cat << EOF > ${CONFIG}
+{
+  "rec_file": "${REC_FILE}",
+  "vm_name": "${LEADING}",
+  "decay": "${DECAY}",
+  "detector": "${JUGGLER_DETECTOR}",
+  "output_prefix": "${RESULTS_PATH}/${PLOT_TAG}",
+  "test_tag": "${LEADING}_${DECAY}_${BEAM_TAG}"
+}
+EOF
+#cat ${CONFIG}
+
+## run the analysis script with this configuration
+root -b -q "dvmp/analysis/vm_mass.cxx(\"${CONFIG}\")"
 
 if [ "$?" -ne "0" ] ; then
   echo "ERROR running root script"
diff --git a/dvmp/env.sh b/dvmp/env.sh
index d449f479..17ef386b 100644
--- a/dvmp/env.sh
+++ b/dvmp/env.sh
@@ -5,6 +5,7 @@
 ## It defines the following additional variables: 
 ##
 ##  - BENCHMARK_TAG:          Tag to identify this particular benchmark
+##  - BEAM_TAG                Tag to identify the beam configuration
 ##  - INPUT_PATH:             Path for generator-level input to the benchmarks
 ##  - TMP_PATH:               Path for temporary data (not exported as artifacts)
 ##  - RESULTS_PATH:           Path for benchmark output figures and files
@@ -21,22 +22,25 @@
 export BENCHMARK_TAG="dvmp"
 echo "Setting up the local environment for the ${BENCHMARK_TAG^^} benchmarks"
 
+## Extra beam tag to identify the desired beam configuration
+export BEAM_TAG="${EBEAM}on${PBEAM}"
+
 ## Data path for input data (generator-level hepmc file)
-INPUT_PATH="input/${BENCHMARK_TAG}/${EBEAM}on${PBEAM}"
+INPUT_PATH="input/${BENCHMARK_TAG}/${BEAM_TAG}"
 mkdir -p ${INPUT_PATH}
 export INPUT_PATH=`realpath ${INPUT_PATH}`
 echo "INPUT_PATH:             ${INPUT_PATH}"
 
 
 ## Data path for temporary data (not exported as artifacts)
-TMP_PATH=${LOCAL_PREFIX}/tmp/${EBEAM}on${PBEAM}
+TMP_PATH=${LOCAL_PREFIX}/tmp/${BEAM_TAG}
 mkdir -p ${TMP_PATH}
 export TMP_PATH=`realpath ${TMP_PATH}`
 echo "TMP_PATH:               ${TMP_PATH}"
 
 ## Data path for benchmark output (plots and reconstructed files
 ## if not too big).
-RESULTS_PATH="results/${BENCHMARK_TAG}/${EBEAM}on${PBEAM}"
+RESULTS_PATH="results/${BENCHMARK_TAG}/${BEAM_TAG}"
 mkdir -p ${RESULTS_PATH}
 export RESULTS_PATH=`realpath ${RESULTS_PATH}`
 echo "RESULTS_PATH:           ${RESULTS_PATH}"
diff --git a/util/benchmark.hh b/util/benchmark.hh
new file mode 100644
index 00000000..aea070f6
--- /dev/null
+++ b/util/benchmark.hh
@@ -0,0 +1,114 @@
+#ifndef BENCHMARK_LOADED
+#define BENCHMARK_LOADED
+
+#include "exception.hh"
+#include <fmt/core.h>
+#include <fstream>
+#include <iostream>
+#include <nlohmann/json.hpp>
+#include <string>
+
+// Bookkeeping of test data to store data of one or more tests in a json file to
+// facilitate future accounting.
+//
+// Usage Example 1 (single test):
+// ==============================
+// 1. define our test
+//      juggler_util::test test1{
+//        {{"name", "example_test"},
+//        {"title", "Example Test"},
+//        {"description", "This is an example of a test definition"},
+//        {"quantity", "efficiency"},
+//        {"target", "1"}}};
+// 2. set pass/fail/error status and return value (in this case .99)
+//      test1.pass(0.99)
+// 3. write our test data to a json file
+//      juggler_util::write_test(test1, "test1.json");
+//
+// Usage Example 2 (multiple tests):
+// =================================
+// 1. define our tests
+//      juggler_util::test test1{
+//        {{"name", "example_test"},
+//        {"title", "Example Test"},
+//        {"description", "This is an example of a test definition"},
+//        {"quantity", "efficiency"},
+//        {"target", "1"}}};
+//      juggler_util::test test2{
+//        {{"name", "another_test"},
+//        {"title", "Another example Test"},
+//        {"description", "This is a second example of a test definition"},
+//        {"quantity", "resolution"},
+//        {"target", "3."}}};
+// 2. set pass/fail/error status and return value (in this case .99)
+//      test1.fail(10)
+// 3. write our test data to a json file
+//      juggler_util::write_test({test1, test2}, "test.json");
+
+// Namespace for utility scripts, FIXME this should be part of an independent
+// library
+namespace juggler_util {
+
+struct test_definition_error : exception {
+  test_definition_error(std::string_view msg)
+      : exception(msg, "test_definition_error") {}
+};
+
+// Wrapper for our test data json, with three methods to set the status
+// after test completion (to pass, fail or error). The default value
+// is error.
+// The following fields should be defined in the definitions json
+// for the test to make sense:
+//  - name: unique identifier for this test
+//  - title: Slightly more verbose identifier for this test
+//  - description: Concise description of what is tested
+//  - quantity: What quantity is tested? Unites of value/target
+//  - target: Target value of <quantity> that we want to reach
+//  - value: Actual value of <quantity>
+//  - weight: Weight for this test (this is defaulted to 1.0 if not specified)
+//  - result: pass/fail/error
+struct test {
+  test(nlohmann::json definition) : json{std::move(definition)} {
+    json["weight"] = 1.0;
+    // initialize with error (as we don't have a value yet)
+    error();
+    // Check that all required fields are present
+    for (const auto& field : {"name", "title", "description", "quantity",
+                              "target", "value", "weight", "result"}) {
+      if (json.find(field) == json.end()) {
+        throw test_definition_error{
+            fmt::format("Error in test definition: field '{}' missing", field)};
+      }
+    }
+  }
+  // Set this test to pass/fail/error
+  void pass(double value) { update_result("pass", value); }
+  void fail(double value) { update_result("fail", value); }
+  void error(double value = 0) { update_result("error", value); }
+
+  nlohmann::json json;
+
+private:
+  void update_result(std::string_view status, double value) {
+    json["result"] = status;
+    json["value"] = value;
+  }
+};
+
+void write_test(const std::vector<test>& data, const std::string& fname) {
+  nlohmann::json test;
+  for (auto& entry : data) {
+    test.push_back(entry.json);
+  }
+  std::cout << fmt::format("Writing test data to {}\n", fname);
+  std::ofstream output_file(fname);
+  output_file << std::setw(4) << test << "\n";
+}
+void write_test(const test& data, const std::string& fname) {
+  std::vector<test> vtd{data};
+  write_test(vtd, fname);
+}
+
+} // namespace juggler_util
+
+#endif
diff --git a/util/exception.hh b/util/exception.hh
new file mode 100644
index 00000000..a443c795
--- /dev/null
+++ b/util/exception.hh
@@ -0,0 +1,23 @@
+#ifndef UTIL_EXCEPTION
+#define UTIL_EXCEPTION
+
+#include <exception>
+#include <string>
+
+namespace juggler_util {
+class exception : public std::exception {
+public:
+  exception(std::string_view msg, std::string_view type = "exception")
+      : msg_{msg}, type_{type} {}
+
+  virtual const char* what() const throw() { return msg_.c_str(); }
+  virtual const char* type() const throw() { return type_.c_str(); }
+  virtual ~exception() throw() {}
+
+private:
+  std::string msg_;
+  std::string type_;
+};
+} // namespace juggler_util
+
+#endif
diff --git a/util/run_many.py b/util/run_many.py
index 45150c08..e101fee8 100755
--- a/util/run_many.py
+++ b/util/run_many.py
@@ -58,9 +58,10 @@ def worker(command):
         cmd = [command, ' 2>&1 >', f.name]
         cmd = ' '.join(cmd)
         print("Executing '{}'".format(cmd))
-        os.system(cmd)
+        ret = os.system(cmd)
         with open(f.name) as log:
             print(log.read())
+        return ret
 
 if __name__ == '__main__':
     args = parser.parse_args()
@@ -112,6 +113,9 @@ if __name__ == '__main__':
     ## a context where subprocesses are created using the new "spawn" process
     ## which avoids deadlocks that sometimes happen in the default dispatch
     with get_context('spawn').Pool(processes=args.nproc) as pool:
-        pool.map(worker, cmds)
+        return_values = pool.map(worker, cmds)
+        ## check if we all exited nicely, else exit with status 1
+        if not all(ret == 0 for ret in return_values):
+            exit(1)
 
     ## That's all!
-- 
GitLab