Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-09-27 09:14:57

0001 // lAger: General Purpose l/A-event Generator
0002 // Copyright (C) 2016-2021 Sylvester Joosten <sjoosten@anl.gov>
0003 //
0004 // This file is part of lAger.
0005 //
0006 // lAger is free software: you can redistribute it and/or modify
0007 // it under the terms of the GNU General Public License as published by
0008 // the Free Shoftware Foundation, either version 3 of the License, or
0009 // (at your option) any later version.
0010 //
0011 // lAger is distributed in the hope that it will be useful,
0012 // but WITHOUT ANY WARRANTY; without even the implied warranty of
0013 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0014 // GNU General Public License for more details.
0015 //
0016 // You should have received a copy of the GNU General Public License
0017 // along with lAger.  If not, see <https://www.gnu.org/licenses/>.
0018 //
0019 
0020 #ifndef LAGER_CORE_GENERATOR_LOADED
0021 #define LAGER_CORE_GENERATOR_LOADED
0022 
0023 #include <TRandom.h>
0024 #include <algorithm>
0025 #include <lager/core/assert.hh>
0026 #include <lager/core/configuration.hh>
0027 #include <lager/core/factory.hh>
0028 #include <lager/core/interval.hh>
0029 #include <memory>
0030 
0031 namespace lager {
0032 
0033 // =============================================================================
0034 // Base class for all generator data
0035 // =============================================================================
0036 class generator_data {
0037 public:
0038   explicit generator_data(const double xs = 1.) : cross_section_{xs} {}
0039 
0040   double cross_section() const { return cross_section_; }
0041   double jacobian() const { return jacobian_; }
0042 
0043   void update_cross_section(const double xs) { cross_section_ *= xs; }
0044   void update_jacobian(const double j) { jacobian_ *= j; }
0045 
0046 private:
0047   double cross_section_;
0048   double jacobian_{1.}; // jacobian to transform to more sane coordinate
0049                         // system, if necessary
0050 };
0051 
0052 // =============================================================================
0053 // Base class for all generators
0054 //
0055 // Owns a shared pointer to the random generator.
0056 // =============================================================================
0057 template <class Data, class... Input> class generator {
0058 public:
0059   using data_type = Data;
0060 
0061   generator(std::shared_ptr<TRandom> r) : rng_{std::move(r)} {}
0062 
0063   // generate an event
0064   virtual data_type generate(const Input&... input) = 0;
0065   data_type operator()(const Input&... input) { return generate(input...); }
0066 
0067   // contribution to the maximum cross section and phase space volume
0068   // (to be implemented by child class)
0069   virtual double max_cross_section() const = 0;
0070   virtual double phase_space() const = 0;
0071 
0072 protected:
0073   // access the RNG
0074   std::shared_ptr<TRandom> rng() const { return rng_; }
0075 
0076   // generate a random number following an arbitrary function
0077   // parameters:
0078   //  range: generation range
0079   //  f: arbitrary function (continous within our range)
0080   //  fmax: maximum of f within our range
0081   // this is useful for e.g. azimuthal distributions that don't impact the total
0082   // cross section estimate
0083   template <class Func1D>
0084   double rand_f(const interval<double>& range, Func1D f, double fmax) const {
0085     const double x = rng()->Uniform(range.min, range.max);
0086     const double test = rng()->Uniform(0, fmax);
0087     const double fx = f(x);
0088     // if the test fails, try again (accept-reject)
0089     if (test > fx) {
0090       return rand_f(range, f, fmax);
0091     }
0092     // ensure a proper fmax
0093     tassert(fx <= fmax, "fmax set too small in rand_f call");
0094 
0095     return x;
0096   }
0097   // same for 2D functions
0098   template <class Func2D>
0099   std::pair<double, double> rand_f(const interval<double>& range1,
0100                                    const interval<double>& range2, Func2D f,
0101                                    double fmax) const {
0102     const double x = rng()->Uniform(range1.min, range1.max);
0103     const double y = rng()->Uniform(range2.min, range2.max);
0104     const double test = rng()->Uniform(0, fmax);
0105     const double fxy = f(x, y);
0106     // if the test fails, try again (accept-reject)
0107     if (test > fxy) {
0108       return rand_f(range1, range2, f, fmax);
0109     }
0110     // ensure a proper fmax
0111     tassert(fxy <= fmax, "fmax set too small in rand_f call (" +
0112                              std::to_string(fxy) + ">" + std::to_string(fmax) +
0113                              ")");
0114     return {x, y};
0115   }
0116 
0117 private:
0118   std::shared_ptr<TRandom> rng_;
0119 };
0120 
0121 // =============================================================================
0122 // Base class for all process_generators
0123 //
0124 // Generator input: initial reaction information
0125 // Generator output: a valid event
0126 //
0127 // Note:
0128 //    * Event should derive from the event class (in core/event.hh)
0129 //    * InitialData should derive from generator_data
0130 // =============================================================================
0131 template <class Event, class InitialData>
0132 class process_generator : public generator<Event, InitialData> {
0133 public:
0134   using event_type = Event;
0135   using initial_type = InitialData;
0136   using base_type = generator<Event, InitialData>;
0137 
0138   static factory<process_generator, const configuration&, const string_path&,
0139                  std::shared_ptr<TRandom>>
0140       factory_instance;
0141 
0142   process_generator(std::shared_ptr<TRandom> r) : base_type{std::move(r)} {}
0143 
0144   virtual event_type generate(const initial_type&) = 0;
0145 };
0146 
0147 template <class Event, class InitialData>
0148 factory<process_generator<Event, InitialData>, const configuration&,
0149         const string_path&, std::shared_ptr<TRandom>>
0150     process_generator<Event, InitialData>::factory_instance;
0151 
0152 // =============================================================================
0153 // Base class for all event_processors (detectors/decay_handlers/...)
0154 //
0155 // Processor input: a valid event
0156 // The processor will modify the input event
0157 //
0158 // Note: Event should derive from the event class (in core/event.hh)
0159 // =============================================================================
0160 template <class Event> class event_processor : public generator<void> {
0161 public:
0162   using event_type = Event;
0163   using base_type = generator<void>;
0164 
0165   event_processor(std::shared_ptr<TRandom> r) : base_type{std::move(r)} {}
0166 
0167   virtual void process(event_type&) const = 0;
0168 
0169 private:
0170   virtual void generate() {}
0171   virtual double max_cross_section() const { return -1.; }
0172   virtual double phase_space() const { return -1.; }
0173 };
0174 
0175 // =============================================================================
0176 // Base class for event generators that handle the following steps:
0177 //    * generate initial state
0178 //    * evaluate all processes (up to 10) simulataneously
0179 //    * accept-reject each process
0180 //    * event building for each process
0181 // Keeps track of the generated cross section
0182 //
0183 // Usage:
0184 //    * the user should derive from this class, and provide a definition of
0185 //    the
0186 //      virtual member functions
0187 //        - InitialData generate_initial();
0188 //        - void build_event(Event&);
0189 //      the rest of the generation process will be handled automatically
0190 //
0191 // Note:
0192 //    * Event should derive from the event class (in core/event.hh)
0193 //    * InitialData should derive from generator_data
0194 // =============================================================================
0195 template <class Event, class InitialData>
0196 class event_generator : public generator<std::vector<Event>>,
0197                         public configurable {
0198 public:
0199   using event_type = Event;
0200   using initial_type = InitialData;
0201   using base_type = generator<std::vector<Event>>;
0202   using process_type = process_generator<event_type, initial_type>;
0203 
0204   event_generator(const configuration& cf, const string_path& path,
0205                   std::shared_ptr<TRandom> r)
0206       : base_type{std::move(r)}
0207       , configurable{cf, path}
0208       , penalty_{cf.get<double>(path / "advanced/penalty", 1.0)} {
0209     init_process_list();
0210     init_lumi(cf);
0211     LOG_INFO("event_generator",
0212              "advanced/penalty: " + std::to_string(penalty_));
0213   }
0214 
0215   virtual std::vector<event_type> generate() {
0216     // buffer for the final events we return
0217     // this is needed because we do one additional accept-reject step where we
0218     // remove events to compensate for a weight smaller than 1, which can
0219     // occur when, e.g., the event builder only simulates one particular decay
0220     // channel
0221     std::vector<event_type> good_event_list;
0222     do {
0223 
0224       // generate a phase space point
0225       std::vector<event_type> event_list;
0226       do {
0227         n_trials_ += 1;
0228         auto initial = generate_initial();
0229         // start over if we already have a bad initial state
0230         if (initial.cross_section() <= 0) {
0231           LOG_JUNK("event_generator",
0232                    "Initial cross section <= 0, abandoning trial cycle.");
0233           continue;
0234         }
0235         LOG_JUNK("event_generator",
0236                  "Initial cross section: " +
0237                      std::to_string(initial.cross_section()));
0238         // generate the sub_processes
0239         for (auto& process : process_list_) {
0240           LOG_JUNK(process.name, "Generating a trial event");
0241           // check if we need to generate an event for this process (ensure
0242           // correct sub-process mixing)
0243           if (proc_volume_ != process.vol &&
0244               this->rng()->Uniform(0, proc_volume_) > process.vol) {
0245             LOG_JUNK(process.name, "Skipping this trial cycle.");
0246             continue;
0247           }
0248           // generate one event
0249           auto event = process.gen->generate(initial);
0250           // go to the next process have a bad cross section, print a warning
0251           // if the cross section maximum was violated
0252           if (event.cross_section() <= 0) {
0253             LOG_JUNK(process.name,
0254                      "Cross section <= 0, skipping this trial cycle");
0255             continue;
0256           } else if (event.cross_section() >
0257                      initial_max_ * process.max * penalty_) {
0258             LOG_WARNING(
0259                 process.name,
0260                 "Cross section maximum exceeded (" +
0261                     std::to_string(event.cross_section()) + " > " +
0262                     std::to_string(initial_max_ * process.max * penalty_) +
0263                     "), the distributions will be invalid if this "
0264                     "happens too often.");
0265             LOG_WARNING(
0266                 process.name,
0267                 "To mitigate, either increase "
0268                 "the configuration paramater generator/advanced/penalty "
0269                 "(which gets multiplied with the cross section maximum "
0270                 "during the accept-reject step), or fix the cross "
0271                 "section maximum estimation in the actual Physics "
0272                 "module.");
0273           }
0274           // accept/reject this event
0275           LOG_JUNK(process.name,
0276                    "Testing accept reject for xs: " +
0277                        std::to_string(event.cross_section()) + " (max: " +
0278                        std::to_string(process.max * initial_max_) + ")");
0279           if (this->rng()->Uniform(0, initial_max_ * process.max * penalty_) <
0280               event.cross_section()) {
0281             LOG_JUNK(process.name, "Event accepted!");
0282             event.update_process(process.id);
0283             event_list.push_back(event);
0284             n_gen_events_ += 1;
0285           } else {
0286             LOG_JUNK(process.name, "Event rejected.");
0287           }
0288         }
0289       } while (event_list.empty());
0290 
0291       for (auto& event : event_list) {
0292         LOG_JUNK("generator", "Processing event (process " +
0293                                   std::to_string(event.process()) + ")");
0294         build_event(event);
0295         if (event.weight() > 0) {
0296           LOG_JUNK("generator",
0297                    "Event accepted after event builder step (weight: " +
0298                        std::to_string(event.weight()) + ", reset to 1)");
0299           n_events_ += 1;
0300           // update BR, assumed to be same for all events!
0301           // TODO this is really a design issue and should be fixed for a next
0302           //      major release
0303           branching_ratio_ = event.weight();
0304           event.update_stat(static_cast<size_t>(n_events_), cross_section());
0305           event.reset_weight();
0306           good_event_list.push_back(event);
0307         } else {
0308           LOG_JUNK("generator",
0309                    "Event rejected after event builder step (weight: " +
0310                        std::to_string(event.weight()) + ")");
0311         }
0312       }
0313       // ensure we actually have an event, else start over
0314     } while (good_event_list.empty());
0315 
0316     return good_event_list;
0317   }
0318 
0319   // total cross section is given by the size of the generator box
0320   // (phase_space * max_cross_section) times the fraction of accepted events
0321   // compared to the number of trials
0322   //
0323   // THis is true for all processes, as we rescaled the number of trials T2
0324   // for a process with less generation volume V2 compared to the prime
0325   // process by a factor of V2/V1, i.e. T2 = V2/V1 * T1
0326   //
0327   // Therefore we get that
0328   // sigma_2 = G2 / T2 * V2 = G2 / T1 * V1
0329   //
0330   // n_tot_events is the sum of all the generated events in all subprocesses,
0331   // hence we obtain sigma_tot = sum_i sigma_i = sum_i G_i * V1/T1
0332   double cross_section() const {
0333     // return a safe upper boundary in case we don't have enough events yet to
0334     // have some kind of reasonable estimate
0335     if (n_events_ < 50) {
0336       return volume_ * process_list_.size();
0337     }
0338     // the actual cross section estimate
0339     return volume_ * n_events() / n_trials_;
0340   }
0341   double partial_cross_section() const {
0342     return cross_section() * branching_ratio_;
0343   }
0344   int64_t n_events() const { return n_events_; }
0345   // return the acceptance, i.e., the number of events divided by the number
0346   // of generated events before the event builder step
0347   double acceptance() const { return double(n_events_) / n_gen_events_; }
0348 
0349   // calculate the number of requested events from the lumi * cross section *
0350   // branching ratio, or alternatively use the fixed number of events
0351   int64_t n_requested() const {
0352     return (n_requested_ > 0) ? n_requested_
0353                               : static_cast<int64_t>(std::round(
0354                                     lumi_ * partial_cross_section()));
0355   }
0356 
0357   bool finished() const { return (n_events() >= n_requested()); }
0358 
0359 protected:
0360   // GENERATION STEPS
0361   // 1. generate the initial reaction, to be implemented by child class
0362   virtual initial_type generate_initial() const = 0;
0363   // 2. "event builder" step, to be implemented by child class
0364   virtual void build_event(event_type&) const = 0;
0365 
0366   // register an initial state  sub-generator (not a process generator) with
0367   // this event generator. This stores the relevant phase_space and
0368   // max_cross_section variables with the event generator
0369   template <class InitialGen>
0370   void register_initial(const std::shared_ptr<InitialGen>& gen) {
0371     LOG_DEBUG("event_generator",
0372               "Registering phase space and cross section max");
0373     tassert(gen, "Requested generator is a null pointer");
0374     initial_ps_ *= gen->phase_space();
0375     initial_max_ *= gen->max_cross_section();
0376     LOG_DEBUG("event_generator",
0377               "New initial cross section max: " + std::to_string(initial_max_));
0378     LOG_DEBUG("event_generator",
0379               "New initial phase space: " + std::to_string(initial_ps_));
0380     update_volume();
0381   }
0382 
0383 private:
0384   constexpr static const int N_MAX_PROC{10}; // maximum number of sub processes;
0385   constexpr static const char* PROC_KEY{"process_"}; // config file key
0386   static std::string process_id(const int i) {
0387     return PROC_KEY + std::to_string(i);
0388   }
0389 
0390   // the maximum cross section and total phase space volume functions
0391   // don't make sense here, as they are different for each of the
0392   // sub-processes
0393   virtual double max_cross_section() const { return -1; }
0394   virtual double phase_space() const { return -1; }
0395 
0396   // update the total generation volume
0397   void update_volume() {
0398     volume_ = initial_ps_ * initial_max_ * proc_volume_ * penalty_;
0399   }
0400 
0401   // initialize the process list
0402   // the factory will construct a new process generator for each of the
0403   // configuration file entries
0404   void init_process_list() {
0405     LOG_INFO("event_generator", "Initializing the process list");
0406     for (int i = 0; i < N_MAX_PROC; ++i) {
0407       const string_path path{PROC_KEY + std::to_string(i)};
0408       // shortcut to avoid overload of template keywords
0409       const configuration& cf = conf();
0410       // check if the process is requested
0411       auto type = cf.get_optional<std::string>(path / "type");
0412       if (type) {
0413         LOG_DEBUG(path.str(),
0414                   "Creating a new process sub-generator (" + *type + ")");
0415         process_list_.push_back(
0416             {i, FACTORY_CREATE(process_type, cf, path, this->rng())});
0417         LOG_DEBUG(path.str(), "Cross section max: " +
0418                                   std::to_string(process_list_.back().max));
0419         LOG_DEBUG(path.str(),
0420                   "Phase space: " + std::to_string(process_list_.back().ps));
0421         // check if we have a larger generation volume, update if needed
0422         const double volume = process_list_.back().vol;
0423         if (volume > proc_volume_) {
0424           LOG_DEBUG("event_generator",
0425                     "Updating maximum process generation volume: " +
0426                         std::to_string(volume));
0427           proc_volume_ = volume;
0428           update_volume();
0429         }
0430       } else {
0431         LOG_JUNK(path.str(), "Not requested");
0432       }
0433     }
0434     tassert(process_list_.size() > 0,
0435             "At least one process has to be specified");
0436   }
0437 
0438   // init the number of requested events, or alternatively the requested
0439   // integrated luminosity
0440   void init_lumi(const configuration& cf) {
0441     auto lumi = cf.get_optional<double>("lumi");
0442     if (lumi) {
0443       lumi_ = *lumi * 1e6; // conversion from fb^-1 to nb^-1
0444       n_requested_ = -1;
0445     } else {
0446       n_requested_ = cf.get<int>("events");
0447       lumi_ = -1;
0448     }
0449   }
0450 
0451   struct process_info {
0452     const int id;                      // process identifier
0453     const std::string name;            // process name
0454     double ps{0};                      // process dependent phase space
0455     double max{0};                     // max cross section
0456     double vol{0};                     // generation volume
0457     double n_events{0};                // number of events
0458     std::shared_ptr<process_type> gen; // process sub-generator
0459     process_info(const int id, std::shared_ptr<process_type> g)
0460         : id{id}
0461         , name{process_id(id)}
0462         , ps{g->phase_space()}
0463         , max{g->max_cross_section()}
0464         , vol{max * ps}
0465         , gen{g} {}
0466   };
0467 
0468   // advanced settings
0469   const double penalty_; // AR max penalty factor
0470 
0471   // Generator state
0472   double initial_ps_{1.};   // initial state generator phase space
0473   double initial_max_{1.};  // initial state generator max cross section
0474   double proc_volume_{-1.}; // largest generation volume in process_list
0475   double volume_{1.};       // total volume
0476 
0477   double n_trials_{0.};        // global trial counter
0478   double n_events_{0};         // total number of events
0479   double n_gen_events_{1};     // raw number of events before event builder step
0480   double branching_ratio_{1.}; // constant branching ratio
0481   std::vector<process_info> process_list_; // process dependent info
0482 
0483   int64_t n_requested_{-1}; // number of requested events
0484   double lumi_{-1}; // or alternatively, the requested luminosity (in fb^-1)
0485 };
0486 
0487 } // namespace lager
0488 #endif