Skip to content
Snippets Groups Projects
Commit a116f39e authored by Whitney Armstrong's avatar Whitney Armstrong
Browse files

Removed a lot of stuff that is now in common_bench

See
[common_bench](https://eicweb.phy.anl.gov/EIC/benchmarks/common_bench/)
parent 032608c4
No related branches found
No related tags found
1 merge request!28Resolve "Cleanup old junk files"
...@@ -9,8 +9,15 @@ Detector benchmarks are meant to test for regressions in individual detector sub ...@@ -9,8 +9,15 @@ Detector benchmarks are meant to test for regressions in individual detector sub
The analysis is meant to avoid a reconstruction step. The analysis is meant to avoid a reconstruction step.
So this precludes using [juggler](https://eicweb.phy.anl.gov/EIC/juggler) for processing the events. So this precludes using [juggler](https://eicweb.phy.anl.gov/EIC/juggler) for processing the events.
## Documentation
See [common_bench](https://eicweb.phy.anl.gov/EIC/benchmarks/common_bench/).
## Adding new benchmarks ## Adding new benchmarks
See `benchmarks` directory for examples.
### Pass/Fail tests ### Pass/Fail tests
- Create a script that returns exit status 0 for success. - Create a script that returns exit status 0 for success.
......
#!/bin/bash
set -o nounset
set -o errexit
CI_TAG=sodium
BENCHMARK_SCRIPT_DIR=.
CI_JOB_PREFIX=cal_test_
function print_the_help {
echo "USAGE: $0 [-t <runner_tag>] "
echo " OPTIONS: "
echo " -i,--input Input scripts directory "
echo " -t,--tag Gitlab Runner tag"
echo " -p,--prefix job name prefix"
exit
}
POSITIONAL=()
while [[ $# -gt 0 ]]
do
key="$1"
case $key in
-h|--help)
shift # past argument
print_the_help
;;
-i|--input)
BENCHMARK_SCRIPT_DIR="$2"
shift # past argument
shift # past value
;;
-t|--tag)
CI_TAG="$2"
shift # past argument
shift # past value
;;
-p|--prefix)
CI_JOB_PREFIX="$2"
shift # past argument
shift # past value
;;
*) # unknown option
#POSITIONAL+=("$1") # save it in an array for later
echo "unknown option $1"
print_the_help
shift # past argument
;;
esac
done
set -- "${POSITIONAL[@]}" # restore positional parameters
shopt -s nullglob
ifile=0
cat <<EOF
stages:
#- simulate
- benchmarks
EOF
for script_name in ${BENCHMARK_SCRIPT_DIR}/*.sh
do
filename=$(basename ${script_name})
filename_noext="${filename%%.*}"
ifile=$((ifile+1))
cat <<EOF
${CI_JOB_PREFIX}${ifile}_${filename_noext}:
stage: benchmarks
script:
- bash ${script_name}
artifact:
paths:
- results/
allow_failure: true
EOF
done
for script_name in ${BENCHMARK_SCRIPT_DIR}/*.cxx
do
filename=$(basename ${script_name})
filename_noext="${filename%%.*}"
ifile=$((ifile+1))
cat <<EOF
${CI_JOB_PREFIX}${ifile}_${filename_noext}:
stage: benchmarks
script:
- root -b -q ${script_name}
artifact:
paths:
- results/
allow_failure: true
EOF
done
#ifndef BENCHMARK_H
#define BENCHMARK_H
#include "exception.h"
#include <fmt/core.h>
#include <fstream>
#include <iomanip>
#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
// eic::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
// eic::util::write_test(test1, "test1.json");
//
// Usage Example 2 (multiple tests):
// =================================
// 1. define our tests
// eic::util::Test test1{
// {{"name", "example_test"},
// {"title", "Example Test"},
// {"description", "This is an example of a test definition"},
// {"quantity", "efficiency"},
// {"target", "1"}}};
// eic::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
// eic::util::write_test({test1, test2}, "test.json");
// Namespace for utility scripts, FIXME this should be part of an independent
// library
namespace eic::util {
struct TestDefinitionError : Exception {
TestDefinitionError(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 {
// note: round braces for the json constructor, as else it will pick the wrong
// initializer-list constructur (it will put everything in an array)
Test(const std::map<std::string, std::string>& definition) : json(definition)
{
// std::cout << json.dump() << std::endl;
// 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", "result"}) {
if (json.find(field) == json.end()) {
throw TestDefinitionError{
fmt::format("Error in test definition: field '{}' missing", field)};
}
}
// Default "weight" to 1 if not set
if (json.find("weight") == json.end()) {
json["weight"] = 1.0;
}
}
// 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["tests"].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 eic::util
#endif
This diff is collapsed.
#ifndef UTIL_EXCEPTION_H
#define UTIL_EXCEPTION_H
#include <exception>
#include <string>
namespace eic::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 eic::util
#endif
#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
#ifndef PLOT_H
#define PLOT_H
#include <TCanvas.h>
#include <TColor.h>
#include <TPad.h>
#include <TPaveText.h>
#include <TStyle.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)
{
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("{} GeV on {} GeV", ebeam, pbeam).c_str());
t->SetTextAlign(12);
t->Draw();
}
} // namespace plot
#endif
#ifndef UTIL_H
#define UTIL_H
// TODO: should probably be moved to a global benchmark utility library
#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 if (part == "proton"){
return 0.938272;
} else {
throw unknown_particle_error{part};
}
}
// Get a vector of 4-momenta from raw tracking info, using an externally
// provided particle mass assumption.
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::PxPyPzMVector{};
}
const double p = fabs(1. / track.qOverP);
const double px = p * cos(track.phi) * sin(track.theta);
const double py = p * sin(track.phi) * sin(track.theta);
const double pz = p * cos(track.theta);
return ROOT::Math::PxPyPzMVector{px, py, pz, mass};
});
return momenta;
}
// Get a vector of 4-momenta from the simulation data.
// 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)
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 (size_t i = 0; i < parts.size(); ++i) {
for (size_t 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 magnitude of the momentum of a vector of 4-vectors
inline auto mom(const std::vector<ROOT::Math::PxPyPzMVector>& momenta)
{
std::vector<double> P(momenta.size());
// transform our raw tracker info into proper 4-momenta
std::transform(momenta.begin(), momenta.end(), P.begin(),
[](const auto& mom) { return mom.P(); });
return P;
}
// Calculate the transverse momentum of a vector of 4-vectors
inline auto pt(const std::vector<ROOT::Math::PxPyPzMVector>& momenta)
{
std::vector<double> pt(momenta.size());
// transform our raw tracker info into proper 4-momenta
std::transform(momenta.begin(), momenta.end(), pt.begin(),
[](const auto& mom) { return mom.pt(); });
return pt;
}
// Calculate the azimuthal angle phi of a vector of 4-vectors
inline auto phi(const std::vector<ROOT::Math::PxPyPzMVector>& momenta)
{
std::vector<double> phi(momenta.size());
// transform our raw tracker info into proper 4-momenta
std::transform(momenta.begin(), momenta.end(), phi.begin(),
[](const auto& mom) { return mom.phi(); });
return phi;
}
// Calculate the pseudo-rapidity of a vector of particles
inline auto eta(const std::vector<ROOT::Math::PxPyPzMVector>& momenta)
{
std::vector<double> eta(momenta.size());
// transform our raw tracker info into proper 4-momenta
std::transform(momenta.begin(), momenta.end(), eta.begin(),
[](const auto& mom) { return mom.eta(); });
return eta;
}
//=========================================================================================================
} // namespace util
#endif
#!/bin/bash
## =============================================================================
## 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
## - JUGGLER_N_THREADS: Number of threads/processes to spawn in parallel
## - JUGGLER_RNG_SEED: Random seed for the RNG
##
## 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
if [ ! -n "${JUGGLER_DETECTOR_VERSION}" ] ; then
export JUGGLER_DETECTOR_VERSION="master"
fi
## Number of events that will be processed by the reconstruction
if [ ! -n "${JUGGLER_N_EVENTS}" ] ; then
export JUGGLER_N_EVENTS=100
fi
## Maximum number of threads or processes a single pipeline should use
## (this is not enforced, but the different pipeline scripts should use
## this to guide the number of parallel processes or threads they
## spawn).
if [ ! -n "${JUGGLER_N_THREADS}" ]; then
export JUGGLER_N_THREADS=10
fi
## Random seed for event generation, should typically not be changed for
## reproductability.
if [ ! -n "${JUGGLER_RNG_SEED}" ]; then
export JUGGLER_RNG_SEED=1
fi
## 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}`
## Location of local data for pass data from job to job within pipeline.
## Not saved as artifacts.
if [ ! -n "${LOCAL_DATA_PATH}" ] ; then
export LOCAL_DATA_PATH="/scratch/${CI_PROJECT_NAME}_${CI_PIPELINE_ID}"
fi
## =============================================================================
## Other utility variables that govern how some of the dependent packages
## are built and installed. You should not have to change these.
## 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}`
## detector prefix: prefix for the detector definitions
export DETECTOR_PREFIX="${LOCAL_PREFIX}/detector"
mkdir -p ${DETECTOR_PREFIX}
## detector path: actual detector definition path
export DETECTOR_PATH="${DETECTOR_PREFIX}/${JUGGLER_DETECTOR}"
## build dir for ROOT to put its binaries etc.
export ROOT_BUILD_DIR=$LOCAL_PREFIX/root_build
echo "JUGGLER_DETECTOR: ${JUGGLER_DETECTOR}"
echo "JUGGLER_DETECTOR_VERSION: ${JUGGLER_DETECTOR_VERSION}"
echo "JUGGLER_N_EVENTS: ${JUGGLER_N_EVENTS}"
echo "JUGGLER_N_THREADS: ${JUGGLER_N_THREADS}"
echo "JUGGLER_RNG_SEED: ${JUGGLER_RNG_SEED}"
echo "JUGGLER_INSTALL_PREFIX: ${JUGGLER_INSTALL_PREFIX}"
echo "LOCAL_DATA_PATH: ${LOCAL_DATA_PATH}"
echo "LOCAL_PREFIX: ${LOCAL_PREFIX}"
echo "DETECTOR_PREFIX: ${DETECTOR_PREFIX}"
echo "DETECTOR_PATH: ${DETECTOR_PATH}"
echo "ROOT_BUILD_DIR: ${ROOT_BUILD_DIR}"
## =============================================================================
## 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}
## =============================================================================
## That's all!
echo "Environment setup complete."
#!/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`
## =============================================================================
## 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=singularity:latest -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..."
#!/bin/bash
## =============================================================================
## Download generator & reconstruction artifacts for one or more 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="master"
function print_the_help {
echo "USAGE: -p process [-p process2] [-b git_branch]"
echo "OPTIONS:"
echo " -p,--process Physics process name (can be defined multiple
times)."
echo " -b,--branch Git branch to download artifacts from (D:
$BRANCH)"
echo " -h,--help Print this message"
echo ""
echo " This script will download the relevant generator artifacts needed"
echo " for local testing of the benchmarks."
exit
}
while [ $# -gt 0 ]
do
key="$1"
case $key in
-p|--process)
PROCS+=("$2")
shift # past argument
shift # past value
;;
-b|--branch)
BRANCH="$2"
shift # past argument
shift # past value
;;
-h|--help)
print_the_help
shift
;;
*) # 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
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}: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 -o results_gen.zip
unzip -u -o results_rec.zip
echo "Cleaning up..."
rm results_???.zip
done
popd
echo "All done"
#!/bin/bash
## =============================================================================
## 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 read options/env.sh for more in-depth explanations of the variables
## and how they can be controlled.
source options/env.sh
## =============================================================================
## Step 1: download/update the detector definitions (if needed)
pushd ${DETECTOR_PREFIX}
## We need an up-to-date copy of the detector
## start clean to avoid issues...
if [ -d ${JUGGLER_DETECTOR} ]; then
echo "cleaning up ${JUGGLER_DETECTOR}"
rm -rf ${JUGGLER_DETECTOR}
fi
echo "Fetching ${JUGGLER_DETECTOR}"
git clone -b ${JUGGLER_DETECTOR_VERSION} https://eicweb.phy.anl.gov/EIC/detectors/${JUGGLER_DETECTOR}.git
#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 "cleaning up accelerator"
rm -rf accelerator
fi
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
## 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} -DCMAKE_CXX_STANDARD=17 && make -j30 install || exit 1
cmake ${DETECTOR_PATH} -DCMAKE_INSTALL_PREFIX=${LOCAL_PREFIX} -DCMAKE_CXX_STANDARD=17 && make -j30 install
## =============================================================================
## Step 3: That's all!
echo "Detector build/install complete!"
#!/usr/bin/env python3
"""
Combine the json files from the individual benchmark tests into
a final master json file combining all benchmarks.
Benchmark results are expected to be all json files in the results
directory.
"""
## Our master definition file, the benchmark project directory
MASTER_FILE=r'benchmarks/benchmarks.json'
## Our results directory
RESULTS_PATH=r'results'
## Output json file with all benchmark results
OUTPUT_FILE=r'results/summary.json'
import argparse
import json
from pathlib import Path
## Exceptions for this module
class Error(Exception):
'''Base class for exceptions in this module.'''
pass
class FileNotFoundError(Error):
'''File does not exist.
Attributes:
file: the file name
message: error message
'''
def __init__(self, file):
self.file = file
self.message = 'No such file or directory: {}'.format(file)
class InvalidDefinitionError(Error):
'''Raised for missing keys in the definitions.
Attributes:
key: the missing key
file: the definition file
message: error message
'''
def __init__(self, key, file):
self.key = key
self.file = file
self.message = "key '{}' not found in '{}'".format(key, file)
class InvalidResultError(Error):
'''Raised for invalid benchmark result value.
Attributes:
key: the missing key
value: the invalid value
file: the benchmark definition file
message: error message
'''
def __init__(self, key, value, file):
self.key = key
self.value = value
self.file = file
self.message = "value '{}' for key '{}' invalid in benchmark file '{}'".format(
value, key, file)
def collect_benchmarks():
'''Collect all benchmark results and write results to a single file.'''
print("Collecting all benchmark results")
## load the test definition for this benchmark
results = _load_master()
## collect the test results
results['benchmarks'] = _load_benchmarks()
## calculate aggregate test statistics
results = _aggregate_results(results)
## save results to output file
_save(results)
## Summarize results
for bm in results['benchmarks']:
_print_benchmark(bm)
_print_summary(results)
def _load_master():
'''Load master definition.'''
master_file = Path(MASTER_FILE)
if not master_file.exists():
raise FileNotFoundError(master_file)
print(' --> Loading master definition from:', master_file)
results = None
with master_file.open() as f:
results = json.load(f)
## ensure this is a valid benchmark file
for key in ('name', 'title', 'description'):
if not key in results:
raise InvalidDefinitionError('target', master_file)
return results
def _load_benchmarks():
'''Load all benchmark results from the results folder.'''
print(' --> Collecting all benchmarks')
rootdir = Path(RESULTS_PATH)
results = []
for file in rootdir.glob('*.json'):
print(' --> Loading file:', file, '... ', end='')
with open(file) as f:
bm = json.load(f)
## skip files that don't include test results
if not 'tests' in bm:
print('skipped (does not contain benchmark results).')
continue
## check if these are valid benchmark results,
## raise exception otherwise
for key in ('name', 'title', 'description', 'target', 'n_tests',
'n_pass', 'n_fail', 'n_error', 'maximum', 'sum', 'value',
'result'):
if not key in bm:
raise InvalidDefinitionError(key, file)
if bm['result'] not in ('pass', 'fail', 'error'):
raise InvalidResultError('result', bm['result'], file)
## Append to our test results
results.append(bm)
print('done')
return results
def _aggregate_results(results):
'''Aggregate benchmark results.'''
print(' --> Aggregating benchmark statistics')
results['n_benchmarks'] = len(results['benchmarks'])
results['n_pass'] = len([1 for t in results['benchmarks'] if t['result'] == 'pass'])
results['n_fail'] = len([1 for t in results['benchmarks'] if t['result'] == 'fail'])
results['n_error'] = len([1 for t in results['benchmarks'] if t['result'] == 'error'])
if results['n_error'] > 0:
results['result'] = 'error'
elif results['n_fail'] == 0:
results['result'] = 'pass'
else:
results['result'] = 'fail'
return results
def _save(results):
'''Save aggregated benchmark results'''
ofile = Path(OUTPUT_FILE)
print(' --> Saving results to:', ofile)
with ofile.open('w') as f:
json.dump(results, f, indent=4)
def _print_benchmark(bm):
'''Print benchmark summary to the terminal.'''
print('====================================================================')
print(' Summary for:', bm['title'])
print(' Pass: {}, Fail: {}, Error: {} out of {} total tests'.format(
bm['n_pass'], bm['n_fail'], bm['n_error'],
bm['n_tests']))
print(' Weighted sum: {} / {}'.format(bm['sum'], bm['maximum']))
print(' kBenchmark value: {} (target: {})'.format(
bm['value'], bm['target']))
print(' ===> status:', bm['result'])
def _print_summary(results):
'''Print master benchmark summary to the terminal.'''
print('====================================================================')
print('MASTER BENCHMARK SUMMARY FOR:', results['title'].upper())
print('Pass: {}, Fail: {}, Error: {} out of {} total benchmarks'.format(
results['n_pass'], results['n_fail'], results['n_error'],
results['n_benchmarks']))
print('===> status:', results['result'])
print('====================================================================')
if __name__ == "__main__":
try:
collect_benchmarks()
except Error as e:
print()
print('ERROR', e.message)
#!/usr/bin/env python3
"""
Collect the json files from individual benchmark tests into
a larger json file that combines all benchmark information,
and do additional accounting for the benchmark.
Tests results are expected to have the following file name and directory
structure:
results/<BENCHMARK_NAME>/**/<SOME_NAME>.json
where ** implies we check recursively check all sub-directories of <BENCHMARK_NAME>
Internally, we will look for the "tests" keyword in each of these
files to identify them as benchmark components.
"""
## Our benchmark definition file, stored in the benchmark root directory
BENCHMARK_FILE=r'benchmarks/{}/benchmark.json'
## Our benchmark results directory
RESULTS_PATH=r'results/{}'
## Output json file with benchmark results
OUTPUT_FILE=r'results/{}.json'
import argparse
import json
from pathlib import Path
## Exceptions for this module
class Error(Exception):
'''Base class for exceptions in this module.'''
pass
class FileNotFoundError(Exception):
'''File does not exist.
Attributes:
file: the file name
message: error message
'''
def __init__(self, file):
self.file = file
self.message = 'No such file or directory: {}'.format(file)
class InvalidBenchmarkDefinitionError(Exception):
'''Raised for missing keys in the benchmark definition.
Attributes:
key: the missing key
file: the benchmark definition file
message: error message
'''
def __init__(self, key, file):
self.key = key
self.file = file
self.message = "key '{}' not found in benchmark file '{}'".format(key, file)
class InvalidTestDefinitionError(Exception):
'''Raised for missing keys in the test result.
Attributes:
key: the missing key
file: the test result file
message: error message
'''
def __init__(self, key, file):
self.key = key
self.file = file
self.message = "key '{}' not found in test file '{}'".format(key, file)
class InvalidTestResultError(Exception):
'''Raised for invalid test result value.
Attributes:
key: the missing key
value: the invalid value
file: the benchmark definition file
message: error message
'''
def __init__(self, key, value, file):
self.key = key
self.value = value
self.file = file
self.message = "value '{}' for key '{}' invalid in test file '{}'".format(
value, key, file)
parser = argparse.ArgumentParser()
parser.add_argument(
'benchmark',
action='append',
help='One or more benchmarks for which to collect test results.')
def collect_results(benchmark):
'''Collect benchmark tests and write results to file.'''
print("Collecting results for benchmark '{}'".format(benchmark))
## load the test definition for this benchmark
results = _load_benchmark(benchmark)
## collect the test results
results['tests'] = _load_tests(benchmark)
## calculate aggregate test statistics
results = _aggregate_results(results)
## save results to output file
_save(benchmark, results)
## Summarize results
_print_summary(results)
def _load_benchmark(benchmark):
'''Load benchmark definition.'''
benchfile = Path(BENCHMARK_FILE.format(benchmark))
if not benchfile.exists():
raise FileNotFoundError(benchfile)
print(' --> Loading benchmark definition from:', benchfile)
results = None
with benchfile.open() as f:
results = json.load(f)
## ensure this is a valid benchmark file
for key in ('name', 'title', 'description', 'target'):
if not key in results:
raise InvalidBenchmarkDefinitionError('target', benchfile)
return results
def _load_tests(benchmark):
'''Loop over all test results in benchmark folder and return results.'''
print(' --> Collecting all test results')
rootdir = Path(RESULTS_PATH.format(benchmark))
results = []
for file in rootdir.glob('**/*.json'):
print(' --> Loading file:', file, '... ', end='')
with open(file) as f:
new_results = json.load(f)
## skip files that don't include test results
if not 'tests' in new_results:
print('not a test result')
continue
## check if these are valid test results,
## raise exception otherwise
for test in new_results['tests']:
for key in ('name', 'title', 'description', 'quantity', 'target',
'value', 'result'):
if not key in test:
raise InvalidTestDefinitionError(key, file)
if test['result'] not in ('pass', 'fail', 'error'):
raise InvalidTestResultError('result', test['result'], file)
## ensure 'weight' key present, defaulting to 1 in needed
if not 'weight' in test:
test['weight'] = 1.
## Append to our test results
results.append(test)
print('done')
return results
def _aggregate_results(results):
'''Aggregate test results for our benchmark.'''
print(' --> Aggregating benchmark statistics')
results['target'] = float(results['target'])
results['n_tests'] = len(results['tests'])
results['n_pass'] = len([1 for t in results['tests'] if t['result'] == 'pass'])
results['n_fail'] = len([1 for t in results['tests'] if t['result'] == 'fail'])
results['n_error'] = len([1 for t in results['tests'] if t['result'] == 'error'])
results['maximum'] = sum([t['weight'] for t in results['tests']])
results['sum'] = sum([t['weight'] for t in results['tests'] if t['result'] == 'pass'])
if (results['n_tests'] > 0):
results['value'] = results['sum'] / results['maximum']
if results['n_error'] > 0:
results['result'] = 'error'
elif results['value'] >= results['target']:
results['result'] = 'pass'
else:
results['result'] = 'fail'
else:
results['value'] = -1
results['result'] = 'error'
return results
def _save(benchmark, results):
'''Save benchmark results'''
ofile = Path(OUTPUT_FILE.format(benchmark))
print(' --> Saving benchmark results to:', ofile)
with ofile.open('w') as f:
json.dump(results, f, indent=4)
def _print_summary(results):
'''Print benchmark summary to the terminal.'''
print('====================================================================')
print('Summary for:', results['title'])
print('Pass: {}, Fail: {}, Error: {} out of {} total tests'.format(
results['n_pass'], results['n_fail'], results['n_error'],
results['n_tests']))
print('Weighted sum: {} / {}'.format(results['sum'], results['maximum']))
print('Benchmark value: {} (target: {})'.format(
results['value'], results['target']))
print('===> status:', results['result'])
print('====================================================================')
if __name__ == "__main__":
args = parser.parse_args()
for benchmark in args.benchmark:
collect_results(benchmark)
#!/usr/bin/env python3
"""
Compile all root analysis scripts under
benchmarks/<BENCHMARK>/analysis/*.cxx
Doing this step here rather than during the main benchmark script has
multiple advantages:
1. Get feedback on syntax errors early on, without wasting compute resources
2. Avoid race conditions for large benchmarks run in parallel
3. Make it easier to properly handle the root build directory, as
this has to exist prior to our attempt to compile, else all will
fail (this is probably an old bug in root...)
Analysis scripts are expected to have extension 'cxx' and be located in the analysis
subdirectory
"""
## Our analysis path and file extension for glob
ANALYSIS_PATH=r'benchmarks/{}/analysis'
ANALYSIS_EXT = r'cxx'
import argparse
import os
from pathlib import Path
## Exceptions for this module
class Error(Exception):
'''Base class for exceptions in this module.'''
pass
class PathNotFoundError(Exception):
'''Path does not exist.
Attributes:
path: the path name
message: error message
'''
def __init__(self, path):
self.file = file
self.message = 'No such directory: {}'.format(file)
class NoAnalysesFoundError(Exception):
'''Did not find any analysis scripts to complile
Attributes:
path: the analysis path
message: error message
'''
def __init__(self, path):
self.file = file
self.message = 'No analysis found (extension \'{}\' in path: {}'.format(file,
ANALYSIS_EXT)
class CompilationError(Exception):
'''Raised when we failed to compile an analysis script
Attributes:
file: analysis file name
path: analysis path
message: error message
'''
def __init__(self, file):
self.file = file
self.message = "Analysis '{}' failed to compile".format(file)
parser = argparse.ArgumentParser()
parser.add_argument(
'benchmark',
help='A benchmarks for which to compile the analysis scripts.')
def compile_analyses(benchmark):
'''Compile all analysis scripts for a benchmark.'''
print("Compiling all analyis scripts for '{}'".format(benchmark))
## Ensure our build directory exists
_init_build_dir(benchmark)
## Get a list of all analysis scripts
_compile_all(benchmark)
## All done!
print('All analyses for', benchmark, 'compiled successfully')
def _init_build_dir(benchmark):
'''Initialize our ROOT build directory (if using one).'''
print(' --> Initializing ROOT build directory ...')
build_prefix = os.getenv('ROOT_BUILD_DIR')
if build_prefix is None:
print(' --> ROOT_BUILD_DIR not set, no action needed.')
return
## deduce the root build directory
pwd = os.getenv('PWD')
build_dir = '{}/{}/{}'.format(build_prefix, pwd, ANALYSIS_PATH.format(benchmark))
print(" --> Ensuring directory '{}' exists".format(build_dir))
os.system('mkdir -p {}'.format(build_dir))
def _compile_all(benchmark):
'''Compile all analysis for this benchmark.'''
print(' --> Compiling analysis scripts')
anadir = Path(ANALYSIS_PATH.format(benchmark))
if not anadir.exists():
raise PathNotFoundError(anadir)
ana_list = []
for file in anadir.glob('*.{}'.format(ANALYSIS_EXT)):
ana_list.append(file)
print(' --> Compiling:', file, flush=True)
err = os.system(_compile_cmd(file))
if err:
raise CompilationError(file)
if len(ana_list) == 0:
raise NoAnalysesFoundError(anadir)
def _compile_cmd(file):
'''Return a one-line shell command to compile an analysis script.'''
return r'bash -c "root -q -b -e \".L {}+\""'.format(file)
if __name__ == "__main__":
args = parser.parse_args()
compile_analyses(args.benchmark)
#!/bin/bash
## =============================================================================
## Generic utility script to parse command line arguments for the various
## bash scripts that control the CI. This script should be source'd with
## command line arguments from a bash-like (non-POSIX) shell such as
## bash or zsh.
##
## To control some of the functionality of the script, you can set the following
## environment variables prior to calling the script:
## - REQUIRE_DECAY: require the --decay flag to be set
## =============================================================================
## Commented out because this should be taken care of by the
## calling script to not enforce a fixed directory structure.
## 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}
## =============================================================================
## Step 1: Process the command line arguments
function print_the_help {
echo "USAGE: --ebeam E --pbeam E --config C1 --decay D2"
echo " [--config C2 --decay D2 --decay D3 ...]"
echo "REQUIRED ARGUMENTS:"
echo " --ebeam Electron beam energy"
echo " --pbeam Ion beam energy"
echo " --config Generator configuration identifiers (at least one)"
if [ ! -z ${REQUIRE_DECAY} ]; then
echo " --decay Specific decay particle (e.g. muon)."
fi
if [ ! -z ${REQUIRE_LEADING} ]; then
echo " --leading Leading particle of interest (e.g. jpsi)."
fi
echo " -h,--help Print this message"
echo ""
echo " Generate multiple monte carlo samples for a desired process."
exit
}
## Required variables
EBEAM=
PBEAM=
DECAYS=
CONFIG=
while [ $# -gt 0 ]
do
key="$1"
case $key in
--config)
CONFIG="$2"
shift # past argument
shift # past value
;;
--ebeam)
EBEAM="$2"
shift # past argument
shift # past value
;;
--pbeam)
PBEAM="$2"
shift # past argument
shift # past value
;;
--leading)
LEADING="$2"
shift # past argument
shift # past value
;;
--decay)
DECAY="$2"
shift # past argument
shift # past value
;;
-h|--help)
print_the_help
exit 0
;;
*) # unknown option
echo "unknown option"
exit 1
;;
esac
done
if [ -z $CONFIG ]; then
echo "ERROR: CONFIG not defined: --config <config>"
print_the_help
exit 1
elif [ -z $EBEAM ]; then
echo "ERROR: EBEAM not defined: --ebeam <energy>"
print_the_help
exit 1
elif [ -z $PBEAM ]; then
echo "ERROR: PBEAM not defined: --pbeam <energy>"
print_the_help
exit 1
elif [ -z $LEADING ] && [ ! -z $REQUIRE_LEADING ]; then
echo "ERROR: LEADING not defined: --leading <channel>"
print_the_help
exit 1
elif [ ! -z $LEADING ] && [ -z $REQUIRE_LEADING ]; then
echo "ERROR: LEADING flag specified but not required"
print_the_help
exit 1
elif [ -z $DECAY ] && [ ! -z $REQUIRE_DECAY ]; then
echo "ERROR: DECAY not defined: --decay <channel>"
print_the_help
exit 1
elif [ ! -z $DECAY ] && [ -z $REQUIRE_DECAY ]; then
echo "ERROR: DECAY flag specified but not required"
print_the_help
exit 1
fi
## Export the configured variables
export CONFIG
export EBEAM
export PBEAM
if [ ! -z $REQUIRE_LEADING ]; then
export LEADING
fi
if [ ! -z $REQUIRE_DECAY ]; then
export DECAY
fi
#!/bin/bash
echo "JUGGLER_TAG: ${JUGGLER_TAG}"
echo "JUGGLER_DETECTOR: ${JUGGLER_DETECTOR}"
echo "JUGGLER_DETECTOR_VERSION: ${JUGGLER_DETECTOR_VERSION}"
echo "JUGGLER_N_EVENTS: ${JUGGLER_N_EVENTS}"
echo "JUGGLER_N_THREADS: ${JUGGLER_N_THREADS}"
echo "JUGGLER_RNG_SEED: ${JUGGLER_RNG_SEED}"
echo "JUGGLER_INSTALL_PREFIX: ${JUGGLER_INSTALL_PREFIX}"
echo "LOCAL_DATA_PATH: ${LOCAL_DATA_PATH}"
echo "LOCAL_PREFIX: ${LOCAL_PREFIX}"
echo "DETECTOR_PREFIX: ${DETECTOR_PREFIX}"
echo "DETECTOR_PATH: ${DETECTOR_PATH}"
#!/usr/bin/env python3
"""
This script will run a CI generator or processing script for multiple configurations.
Author: Sylvester Joosten <sjoosten@anl.gov>
"""
import os
import argparse
from multiprocessing import Pool, get_context
from tempfile import NamedTemporaryFile
class InvalidArgumentError(Exception):
pass
parser = argparse.ArgumentParser()
parser.add_argument(
'command',
help="Script to be launched in parallel")
parser.add_argument(
'--energy', '-e',
dest='energies',
action='append',
help='One or more beam energy pairs (e.g. 10x100)',
required=True)
parser.add_argument(
'--config', '-c',
dest='configs',
action='append',
help='One or more configurations',
required=True)
parser.add_argument(
'--leading',
dest='leads',
action='append',
help='One or more leading particles(opt.)',
required=False)
parser.add_argument(
'--decay',
dest='decays',
action='append',
help='One or more decay channels (opt.)',
required=False)
parser.add_argument(
'--nproc',
dest='nproc',
default=5,
type=int,
help='Number of processes to launch in parallel',
required=False)
def worker(command):
'''Execute the command in a system call, with the supplied argument string.'''
## use a temporary file to capture the terminal output, and then
## print the terminal output once the command finishes
with NamedTemporaryFile() as f:
cmd = [command, ' 2>&1 >', f.name]
cmd = ' '.join(cmd)
print("Executing '{}'".format(cmd))
ret = os.system(cmd)
with open(f.name) as log:
print(log.read())
return ret
if __name__ == '__main__':
args = parser.parse_args()
print('Launching CI script in parallel for multiple settings')
for e in args.energies:
beam_setting = e.split('x')
if not beam_setting[0].isnumeric() or not beam_setting[1].isnumeric():
print("Error: invalid beam energy setting:", e)
raise InvalidArgumentError
if not os.path.exists(args.command):
print("Error: Script not found:", args.command)
raise InvalidArgumentError
if args.nproc < 1 or args.nproc > 50:
print("Error: Invalid process limit (should be 1-50):", args.nproc)
raise InvalidArgumentError
print(' - command: {}'.format(args.command))
print(' - energies: {}'.format(args.energies))
print(' - config: {}'.format(args.configs))
print(' - nproc: {}'.format(args.nproc))
if (args.leads):
print(' - leading: {}'.format(args.leads))
if (args.decays):
print(' - decay: {}'.format(args.decays))
## Expand our command and argument list for all combinatorics
cmds = []
decays = args.decays if args.decays else [None]
leads = args.leads if args.leads else [None]
for e in args.energies:
for c in args.configs:
for l in leads:
for d in decays:
beam_setting = e.split('x')
cmd = [args.command,
'--ebeam', beam_setting[0],
'--pbeam', beam_setting[1],
'--config', c]
if l is not None:
cmd += ['--leading', l]
if d is not None:
cmd += ['--decay', d]
cmds.append(' '.join(cmd))
## create a process pool
## note that I'm using themultiprocessing.get_context function to setup
## 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:
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):
n_fail = sum([1 for ret in return_values if ret != 0])
print('ERROR, {} of {} jobs failed'.format(n_fail, len(cmds)))
print('Return values:', [ret for ret in return_values if ret != 0])
exit(1)
## That's all!
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment