diff --git a/madspace/generate_pyi.py b/madspace/generate_pyi.py index b83c686e40..465e6fd593 100644 --- a/madspace/generate_pyi.py +++ b/madspace/generate_pyi.py @@ -2,8 +2,8 @@ """Generate madspace/_madspace_py.pyi from a freshly built _madspace_py module. pybind11-stubgen has to import the module to introspect it, which in turn -requires the full "madspace" package (its __init__.py preloads libmadspace -and monkey-patches a few classes). Rather than requiring a full install, this +requires the full "madspace" package (its loader preloads libmadspace and +monkey-patches a few classes). Rather than requiring a full install, this script assembles a throwaway copy of just the pieces needed for that import in --stage-dir, then runs pybind11-stubgen against it. """ @@ -77,9 +77,8 @@ def main(): ) # pybind11-stubgen's own error checks (above) don't guarantee the emitted - # file is syntactically valid Python, e.g. a bad --print-safe-value-reprs - # match can inline an expression verbatim; parse it so a broken stub - # fails the build instead of shipping something downstream tools choke on. + # file is syntactically valid Python (e.g. an expression may be inlined + # verbatim); parse it so a broken stub fails the build. stub_path = args.output_dir / "madspace" / "_madspace_py.pyi" source = stub_path.read_text() try: diff --git a/madspace/include/madspace/compgraphs/function.hpp b/madspace/include/madspace/compgraphs/function.hpp index 42c0a48dd2..1349390554 100644 --- a/madspace/include/madspace/compgraphs/function.hpp +++ b/madspace/include/madspace/compgraphs/function.hpp @@ -31,7 +31,7 @@ struct InstructionCall { /** * An immutable compute graph produced by @ref FunctionBuilder. * - * A function has named `inputs` and `outputs`, a list of intermediate `locals`, + * A function has named `inputs` and `outputs`, a list of `locals`, * named `globals` for trainable or externally supplied tensors, and the ordered * list of @ref InstructionCall that computes the outputs from the inputs. Run * one with a `FunctionRuntime`, or `save` it to disk and `load` it back. @@ -46,7 +46,7 @@ class Function { const NamedVector& inputs() const { return _inputs; } /// Named graph outputs. const NamedVector& outputs() const { return _outputs; } - /// Intermediate values, one per instruction output. + /// All local values: inputs, globals, literals and instruction outputs. const ValueVec& locals() const { return _locals; } /// Named global tensors: trainable parameters and external inputs. const std::vector>& globals() const { @@ -97,7 +97,7 @@ void from_json(const nlohmann::json& j, Function& call); * * Construct it with the input and output types, read the graph inputs with * `input`, add nodes by calling the instruction methods (for example `add`, - * `stack` or `two_body_decay_com`, listed below), wire the results to the + * `stack` or `two_body_decay_com`, generated below), wire the results to the * outputs with `output`, then call `function` to obtain the finished * @ref Function. The instruction methods are generated from * `madspace/instruction_set.yaml`. @@ -108,7 +108,8 @@ class FunctionBuilder { FunctionBuilder( const NamedVector& _input_types, const NamedVector& _output_types ); - /// Start a builder pre-populated with an existing function. + /// Start a builder that has the inputs of an existing function; its + /// instructions are not copied. FunctionBuilder(const Function& function); /// The graph input at position @p index. Value input(int index) const; @@ -134,7 +135,8 @@ class FunctionBuilder { void set_current_stream(std::size_t stream_index) { _current_stream = stream_index; } - /// Freeze the recorded graph into an immutable @ref Function. + /// Freeze the recorded graph into an immutable @ref Function. Instructions + /// that do not contribute to any output are dropped. Function function(); /// Sum of @p values, element by element. diff --git a/madspace/include/madspace/compgraphs/type.hpp b/madspace/include/madspace/compgraphs/type.hpp index 3084f9dc0d..2005e7b366 100644 --- a/madspace/include/madspace/compgraphs/type.hpp +++ b/madspace/include/madspace/compgraphs/type.hpp @@ -72,7 +72,7 @@ class BatchSize { BatchSize operator-(const BatchSize& other) const { return add(other, -1); } bool operator==(const BatchSize& other) const { return value == other.value; } bool operator!=(const BatchSize& other) const { return value != other.value; } - /// Human-readable representation such as `"batch_size + 1"`. + /// Human-readable representation such as `"batch_size+1"`. std::string to_string() const; friend std::ostream& operator<<(std::ostream& out, const BatchSize& batch_size); @@ -187,16 +187,17 @@ using LiteralValue = std::variant * or a reference to a graph local. * * @ref FunctionBuilder methods take and return `Value`s. A value with - * `local_index >= 0` refers to the output of an earlier instruction. Otherwise + * `local_index >= 0` refers to a local of the function being built (an input, + * global, registered literal or instruction output). Otherwise * @p literal_value holds an inline scalar or dense tensor constant. Scalars and - * nested `std::vector`s convert to a `Value` implicitly. + * `std::vector`s (also nested ones) convert to a `Value` implicitly. */ struct Value { /// Type of the value. Type type; - /// Inline constant, or `std::monostate` for a graph local. + /// Inline constant, or `std::monostate` for a graph local without constant value. LiteralValue literal_value; - /// Index of the referenced graph local, or -1 for a literal. + /// Index of the referenced graph local, or -1 if not yet registered as one. int local_index = -1; /// Empty value (no literal, no local); tests as `false`. diff --git a/madspace/include/madspace/driver/adam_optimizer.hpp b/madspace/include/madspace/driver/adam_optimizer.hpp index d276f76610..d91c68441f 100644 --- a/madspace/include/madspace/driver/adam_optimizer.hpp +++ b/madspace/include/madspace/driver/adam_optimizer.hpp @@ -6,9 +6,9 @@ namespace madspace { -/// Compute-graph function that rescales a gradient tensor so its norm never -/// exceeds a threshold; the graph built internally by @ref AdamOptimizer for -/// `grad_clip_threshold`. +/// Compute-graph function that rescales `gradients_in` so that its L2 norm does +/// not exceed `threshold`; also returns the unclipped norm. Used internally by +/// @ref AdamOptimizer for `grad_clip_threshold`. class GradientClipper : public FunctionGenerator { public: GradientClipper(); @@ -27,7 +27,9 @@ class GradientClipper : public FunctionGenerator { * decay, @p eps for numerical stability, optional @p weight_decay) to its * `requires_grad` @ref Context globals. @p schedule optionally decays the * learning rate over @p step_count steps; @p grad_clip_threshold, if - * non-zero, rescales the gradient norm before the update. + * non-zero, clips the gradient norm to `grad_clip_threshold` times the square + * root of the number of trained parameters before the update. A step is skipped + * if the loss is NaN or, after 100 steps, exceeds 20 times its running mean. * * **References** * - [1] D. P. Kingma, J. Ba, "Adam: A method for stochastic optimization", @@ -54,8 +56,10 @@ class AdamOptimizer { * @param beta1 First-moment decay rate. * @param beta2 Second-moment decay rate. * @param eps Numerical-stability constant. - * @param grad_clip_threshold Maximum gradient norm; `0` disables clipping. - * @param weight_decay L2 weight-decay coefficient. + * @param grad_clip_threshold Maximum gradient norm per sqrt(parameter + * count); `0` disables clipping. + * @param weight_decay Decoupled weight decay (parameters shrink + * by `learning_rate * weight_decay` per step). */ AdamOptimizer( const Function& function, @@ -69,8 +73,8 @@ class AdamOptimizer { double grad_clip_threshold = 0.0, double weight_decay = 0.0 ); - /// Run one training step on `inputs` and apply the Adam update; returns - /// `function`'s outputs. + /// Run one training step on `inputs` and apply the Adam update (unless the + /// step is skipped, see above); returns `function`'s outputs. TensorVec step(const TensorVec& inputs); /// Replace the loss function, keeping the optimizer state. void replace_function(const Function& function); diff --git a/madspace/include/madspace/driver/backend.hpp b/madspace/include/madspace/driver/backend.hpp index 85a2073c9d..a4279c524f 100644 --- a/madspace/include/madspace/driver/backend.hpp +++ b/madspace/include/madspace/driver/backend.hpp @@ -36,11 +36,12 @@ class Runtime { /// Owning handle to a `Runtime`. using RuntimePtr = std::unique_ptr; /// Compile `function` into a callable @ref Runtime for `context`'s device. -/// `concurrent` allows several `Runtime`s to share one compilation cache. +/// `concurrent` lets the CPU runtime run independent instructions concurrently on +/// the context's thread pool; the GPU runtime ignores it. RuntimePtr build_runtime(const Function& function, ContextPtr context, bool concurrent = true); -/// Names of the compute backends (CPU, CUDA, HIP, ...) built into this -/// installation. +/// Names of the CPU backend variants usable on this host ("cpu_scalar", +/// "cpu_vec128", "cpu_vec256", ...); GPU backends are not listed. std::vector available_backends(); DevicePtr cpu_device(); DevicePtr cuda_device(std::size_t index = 0); diff --git a/madspace/include/madspace/driver/channel_generator.hpp b/madspace/include/madspace/driver/channel_generator.hpp index 80b03861e6..cbb803f31e 100644 --- a/madspace/include/madspace/driver/channel_generator.hpp +++ b/madspace/include/madspace/driver/channel_generator.hpp @@ -31,8 +31,8 @@ namespace madspace { */ class ChannelEventGenerator { public: - /// Reconstruct a generator previously written with @ref save, reopening - /// `event_file` and `weight_file`. + /// Reconstruct a generator previously written with @ref save. Creates new, + /// empty `event_file` and `weight_file`. static ChannelEventGenerator load( const std::string& channel_file, const std::vector& contexts, @@ -53,8 +53,9 @@ class ChannelEventGenerator { /** * @param contexts One context per device to run on. * @param integrand The channel's compiled integrand. - * @param event_file Path of the weighted event file. - * @param weight_file Path of the max-weight tracking file. + * @param event_file Path of the file the unweighted events are + * written to. + * @param weight_file Path of the file holding one weight per event. * @param config Generator configuration. * @param subprocess_index Index of the owning subprocess. * @param name Human-readable channel name. @@ -87,9 +88,9 @@ class ChannelEventGenerator { std::size_t count_requested_opt() const { return _count_requested_opt; } /// The filled observable histograms, if any were requested. const std::vector& histograms() const { return _histograms; } - /// The weighted event file. + /// The event file. EventFile& event_file() { return _event_file; } - /// The max-weight tracking file. + /// The weight file, one weight per event of @ref event_file. EventFile& weight_file() { return _weight_file; } /// Current maximum-weight estimate, used for unweighting. double max_weight() const { return _max_weight; } @@ -103,8 +104,8 @@ class ChannelEventGenerator { bool has_abs_integral_prior() const { return _abs_integral_prior.has_value(); } /// Set the signed integral known from an earlier run, with its standard error. void set_integral_prior(double mean, double error); - /// Whether this run has too few samples for an integral estimate of its own, so - /// that @ref EventGenerator uses the priors instead. + /// Whether an absolute integral prior is set and this run has too few samples + /// for an estimate of its own, so that @ref EventGenerator uses the priors. bool uses_integral_prior() const; /// The prior of the signed integral as (mean, standard error), or of the /// absolute one if no signed prior was set. @@ -117,7 +118,7 @@ class ChannelEventGenerator { /// with the prior (if any) weighted by the inverse variances. Used to share /// the events between channels. double abs_integral_estimate() const; - /// Current generation batch size. + /// Size of the next optimization batch, see @ref next_vegas_batch_size. std::size_t batch_size() const { return _batch_size; } /// Whether a VEGAS grid or channel-weight optimization pass is still /// pending. @@ -138,16 +139,17 @@ class ChannelEventGenerator { /// Layout of @ref event_file. const DataLayout& event_file_layout() const { return _event_file_layout; } - /// Run a final unweighting pass over @ref weight_file into @ref - /// event_file, using `rand_gen`. + /// Unweight @ref weight_file in place with the current maximum weight: weights + /// of rejected events are set to zero. Uses `rand_gen`. void unweight_file(MixMaxRandom& rand_gen); - /// Integrate `job`'s events, accumulating @ref cross_section. + /// Accumulate `job`'s weights into @ref cross_section, the status and the + /// optimizer and histogram data. void integrate(const GeneratorBatchJob& job); - /// Accumulate `job`'s events into the VEGAS grid and channel-weight - /// optimizers. + /// Update the VEGAS grid and channel-weight optimizers from the data + /// accumulated by @ref integrate, and update the optimization status. void optimize_vegas(const GeneratorBatchJob& job); - /// Sum of this channel's weight over `event_count` freshly sampled events, - /// used for multi-channel weight optimization. + /// Sum of |weight| / max_weight over the first `event_count` nonzero weights + /// of @ref weight_file. double channel_weight_sum(std::size_t event_count); /// Dispatch `job` (a survey or generation batch) onto the thread pool, /// seeded from `seed`. @@ -168,20 +170,22 @@ class ChannelEventGenerator { void start_unweight_job(GeneratorBatchJob& job, ResultQueue& result_queue); /// Size of the next VEGAS/channel-weight optimization batch. std::size_t next_vegas_batch_size(); - /// Discard the accumulated event and weight files. + /// Discard the accumulated events and weights, and reset the integral, the + /// event counts and (unless fixed) the maximum weight. void clear_events(); /// Update @ref max_weight from a batch of `weights`. void update_max_weight(Tensor weights); - /// Append `unweighted_events` to @ref event_file, recording - /// `job_max_weight`. + /// Append `unweighted_events` to @ref event_file and their weights to + /// @ref weight_file. `job_max_weight` is the maximum weight they were + /// unweighted with. void write_events(const TensorVec& unweighted_events, double job_max_weight); /// Serialize this generator's state to `file_name`; see @ref load. void save(const std::string& file_name) const; /// The JSON text that @ref save writes; see @ref load_json. With - /// `include_estimates`, the current maximum weight and the integral of the - /// absolute weights are stored as well. @ref load_json then keeps the - /// maximum weight fixed and uses the integral as a prior, see - /// @ref abs_integral_estimate. + /// `include_estimates`, the current maximum weight and, if there are at least + /// two samples, the integrals (signed and absolute) are stored as well. + /// @ref load_json then keeps the maximum weight fixed and uses the + /// integrals as priors, see @ref abs_integral_estimate. std::string to_json(bool include_estimates = false) const; private: @@ -251,10 +255,9 @@ class ChannelEventGenerator { RunningIntegral _cross_section; RunningIntegral _abs_cross_section; double _max_weight = 0.; - // Expected number of accepted events, sum of min(|w| / max_weight, 1) over the - // committed generate stages, rescaled like count_unweighted when the max - // weight rises. Decides when the max weight is frozen: unlike count_unweighted - // it only depends on the generate stages, which commit in a fixed order. + // Sum of min(|w| / max_weight, 1) over the committed generate stages, rescaled + // when the max weight rises. Decides when the max weight is frozen; unlike + // count_unweighted it does not depend on the unweight stages' commit timing. double _expected_unweighted = 0.; bool _max_weight_fixed = false; // mean and standard error @@ -265,11 +268,8 @@ class ChannelEventGenerator { // never reset. Separate for survey/generate so neither depends on the other. std::size_t _survey_rng_seq = 0; std::size_t _generate_rng_seq = 0; - // Progress of unweight_file()'s final pass over _weight_file: _unweighted_count - // is the index up to which it's been scanned under the *current* _max_weight, - // and _unweighted_accept_count the number of those accepted. Both reset to 0 - // whenever _max_weight changes (forcing a full rescan), since a changed - // max_weight invalidates every prior accept/reject decision. + // Progress of unweight_file(): the number of weights scanned and accepted under + // the current _max_weight. Reset to 0 whenever _max_weight changes. std::size_t _unweighted_count = 0; std::size_t _unweighted_accept_count = 0; std::size_t _iters_without_improvement = 0; diff --git a/madspace/include/madspace/driver/context.hpp b/madspace/include/madspace/driver/context.hpp index 43eb5acab2..327ac3a140 100644 --- a/madspace/include/madspace/driver/context.hpp +++ b/madspace/include/madspace/driver/context.hpp @@ -177,24 +177,19 @@ class MatrixElementApi { /// Opaque per-thread UMAMI process handle, initialized on first use. void* process_instance() const { return _instances.get().get(); } - // The Device this api's library was loaded on (ie the owning Context's - // device). Callers driving umami_matrix_element() need this to know - // whether the inputs/outputs it exchanges have to be staged through - // device-resident buffers (cuda/hip) or can be passed as plain host - // pointers (cpu). + /// The device of the owning @ref Context. It determines whether inputs and + /// outputs of @ref call must be device buffers (CUDA/HIP) or host pointers. DevicePtr device() const noexcept { return _device; } - // Whether the loaded library actually implements umami_set_parameter/ - // umami_get_parameter (both optional symbols; a library built without - // them still loads fine, it just can't be parameter-reweighted in place - // and set_parameter()/get_parameter() will throw UMAMI_ERROR_NOT_IMPLEMENTED - // if called anyway). + /// Whether the library provides the optional `umami_set_parameter`. bool supports_set_parameter() const noexcept { return bool(_functions.set_parameter); } + /// Whether the library provides the optional `umami_get_parameter`. bool supports_get_parameter() const noexcept { return bool(_functions.get_parameter); } - // Directly sets/reads physics parameters according to whatever - // convention the library has implemented. + /// Set a physics parameter (real and imaginary part) of this thread's process + /// instance. Throws if the library does not implement it or rejects `name`. void set_parameter(const std::string& name, double real, double imag = 0.0) const; + /// Get a physics parameter of this thread's process instance as (real, imaginary). std::pair get_parameter(const std::string& name) const; private: @@ -272,8 +267,8 @@ class Context { /// `param_card`. Every entry of `parameters` is then passed to each /// process instance through `umami_set_parameter` (real value, e.g. /// `{"bwcutoff": 15.}`, the window of the `$`-excluded propagators); - /// a library that rejects one of them is an error. Caches by `file`: - /// loading the same file twice returns the same @ref MatrixElementApi. + /// a library that rejects one of them is an error. Each call loads the + /// library again and appends a new @ref MatrixElementApi. const MatrixElementApi& load_matrix_element( const std::string& file, const std::string& param_card, @@ -289,7 +284,8 @@ class Context { const std::string& param_card = "", const std::unordered_map& parameters = {} ); - /// Create and zero-initialize a new global named `name`. + /// Create and zero-initialize a new global named `name`. The stored tensor + /// has `shape` preceded by a batch dimension of size 1. Tensor define_global( const std::string& name, DataType dtype, @@ -308,12 +304,13 @@ class Context { std::vector global_names() const; /// Remove the global named `name`. void delete_global(const std::string& name); - /// Overwrite every global also present in `context` with its value there. + /// Copy every global of `context` into this context, defining the ones that + /// do not exist yet. Data type and shape of existing globals must match. void copy_globals_from(Context& context); - /// Repack the globals in `names` into one contiguous allocation, for - /// example before handing them to an external optimizer. + /// Repack the globals in `names` into one contiguous allocation and return + /// it. The globals must share a data type and have no outside references. Tensor reallocate_globals_contiguously(const std::vector& names); - /// The matrix element previously loaded at position `index`; see @ref + /// The matrix element at position `index` in load order; see @ref /// load_matrix_element. const MatrixElementApi& matrix_element(std::size_t index) const; /// Write every global to the tar archive `file`, as one `.npy` file per name. @@ -398,9 +395,10 @@ ContextPtr default_hip_context(std::size_t index = 0); ContextPtr default_device_context(DevicePtr device); /// GPU stream that runtime calls on this thread run on without synchronizing; -/// 0 for the legacy default stream. +/// 0 for the legacy default stream. Empty if none is set. std::optional caller_stream(); -/// Set the stream returned by @ref caller_stream for this thread. +/// Set the stream returned by @ref caller_stream for this thread. A value of 1 +/// is stored as 0; 2 (per-thread default stream) is rejected. void set_caller_stream(std::optional stream); /// `name`, namespaced under `prefix` as `"prefix.name"`; `name` unchanged if diff --git a/madspace/include/madspace/driver/discrete_optimizer.hpp b/madspace/include/madspace/driver/discrete_optimizer.hpp index b51c86562a..c594fde574 100644 --- a/madspace/include/madspace/driver/discrete_optimizer.hpp +++ b/madspace/include/madspace/driver/discrete_optimizer.hpp @@ -10,10 +10,10 @@ namespace madspace { * Refines the categorical probabilities of a @ref DiscreteSampler or @ref * DiscreteFlow from accumulated per-category statistics. * - * The training-time counterpart of @ref DiscreteHistogram — @ref add_data - * feeds in the per-category weight sums accumulated during a warm-up run, and - * @ref optimize rewrites every `prob_names` global on every context in - * `contexts` with the refined distribution. + * Counterpart of @ref DiscreteHistogram: @ref add_data feeds in the + * per-category weight sums and counts it accumulates, and @ref optimize + * rewrites every `prob_names` global on every context in `contexts` with the + * refined distribution. */ class DiscreteOptimizer { public: diff --git a/madspace/include/madspace/driver/event_generator.hpp b/madspace/include/madspace/driver/event_generator.hpp index 53f4e7f10e..97be9bff9b 100644 --- a/madspace/include/madspace/driver/event_generator.hpp +++ b/madspace/include/madspace/driver/event_generator.hpp @@ -90,8 +90,8 @@ class EventGenerator { * @param systematics Optional; adds the scale/PDF variation weights to * the written events (`.npy` columns `rwgt_`). * @param histograms Optional; filled with the written events and all - * their weights (recorded in `info.json`'s - * `"event_histograms"`). + * their weights (recorded under `"event_histograms"` + * in the status file). */ void combine_to_compact_npy( const std::string& file_name, @@ -153,44 +153,36 @@ class EventGenerator { std::vector _channel_optimizing; std::vector _channel_integral_fractions; std::vector _context_job_counts; - // True while a channel has a steady-state batch dispatched but not yet fully + // True while a channel has a generation batch dispatched but not yet fully // committed; keeps next_batch_event_count() from double-counting in-flight work. std::vector _channel_batch_pending; - // True once a channel's current generation ReadyJob has had its full event count - // dispatched (batch_event_count reached zero). Needed alongside - // channel_job_count == 0 before finish_channel_job() clears _channel_batch_pending - // -- dispatch now happens incrementally, so channel_job_count can transiently hit - // zero mid-batch, between one sub-job's commit and the next one being dispatched. + // True once all events of a channel's current generation ReadyJob are dispatched. + // finish_channel_job() needs this besides channel_job_count == 0, since the count + // can hit zero between one sub-job's commit and the next one's dispatch. std::vector _channel_batch_dispatch_done; - // Round-robin position into _ready_jobs for generation-batch dispatch, persisted - // across start_jobs() calls so multiple channels' batches interleave (one device - // batch at a time) instead of one channel's batch draining before the next is - // touched. + // Round-robin position into _ready_jobs, kept across start_jobs() calls so that + // channels' batches interleave. std::size_t _ready_job_rr_cursor = 0; - // Per-channel commit ordering, analogous to _ready_gen/_commit_cursor but ordered - // per channel instead of globally. _channel_gen_order holds a channel's dispatched - // job ids in dispatch order and _channel_ready_gen the ones that have completed; - // the front of the order deque is the next commit due. An explicit deque rather - // than a "next id" counter because a channel's job ids are not contiguous: dispatch - // round-robins between channels, so consecutive ids belong to different channels. + // Per-channel commit ordering. _channel_gen_order holds a channel's dispatched job + // ids in dispatch order and _channel_ready_gen the completed ones; the front of the + // deque is the next commit due. A deque is needed since a channel's ids are not + // contiguous. std::vector> _channel_gen_order; std::vector> _channel_ready_gen; - // Same, for a job's unweight-stage completion (tracked separately since it's a - // distinct completion event). Order is appended at generate-commit time, which is - // also when the unweight stage is queued. + // Same for the unweight stage; the order is appended when the generate stage + // commits, which is when the unweight stage is queued. std::vector> _channel_unweight_order; std::vector> _channel_unweight_ready; - // generate() only: per-context queue of job ids awaiting unweight-stage - // dispatch, drained with priority by start_jobs(). + // Per-context queue of job ids awaiting unweight-stage dispatch, drained with + // priority by start_jobs(). std::vector> _context_unweight_queue; ResultQueue _result_queue; // Base seed for reproducible event generation. std::uint64_t _seed; - // unweight_all() may run more than once per generate() (a channel's target can - // grow after it looked done, un-finishing it and triggering another round). - // Salted by this counter so repeated calls don't replay the same stream. + // unweight_all() may run more than once per generate(); this counter salts its + // seeds so repeated calls don't replay the same stream. std::size_t _unweight_call_index = 0; // What a run of the generate loop does; see run_generate(). @@ -205,7 +197,7 @@ class EventGenerator { bool _survey_job = false; std::size_t _survey_pass = 0; - // Generate completions are committed in ascending job id, with + // survey() only: generate completions are committed in ascending job id, with // _commit_cursor as the next id due. std::set _ready_gen; std::size_t _commit_cursor = 0; diff --git a/madspace/include/madspace/driver/event_histograms.hpp b/madspace/include/madspace/driver/event_histograms.hpp index 80a59032a6..dba1db881f 100644 --- a/madspace/include/madspace/driver/event_histograms.hpp +++ b/madspace/include/madspace/driver/event_histograms.hpp @@ -17,8 +17,9 @@ namespace madspace { /// One @ref EventHistograms histogram: an observable name and its binning. struct EventHistogramSpec { - /// Observable name; must match a key of every subprocess's @ref - /// SubprocessObservables::values, unless @ref from_weight is set. + /// Histogram name. Observables are matched to specs by position: the n-th + /// spec without @ref from_weight uses the n-th observable of + /// @ref SubprocessObservables::values. std::string name; /// Lower edge of the first bin. double min; @@ -74,16 +75,16 @@ class EventHistograms { /// The histogram definitions passed to the constructor. const std::vector& specs() const { return _specs; } - /// Number of weight columns each histogram carries (nominal plus every - /// systematic variation). + /// Number of systematic variation weights per event, in addition to the + /// nominal weight. Zero until the first call of @ref fill. std::size_t weight_count() const { return _weight_count; } /** * Fill from a batch of combined events. * * `syst_weights` holds `weight_count` variation weights per event - * (row-major), as computed by @ref SystematicsCalculator. Column 0 of the - * histograms is the nominal weight. Thread-safe. + * (row-major), as computed by @ref SystematicsCalculator; the nominal + * weight is taken from `buffer`. Thread-safe. */ void fill( EventBuffer& buffer, @@ -109,7 +110,8 @@ class EventHistograms { double _reference_weight = 1.; std::vector> _runtimes; std::size_t _weight_count = 0; - // sums[observable][weight column][bin], bins: underflow, bin_count, overflow + // sums[spec][weight column][bin], bins: underflow, bin_count, overflow; + // column 0 is the nominal weight std::vector>> _sums; std::vector>> _square_sums; std::size_t _event_count = 0; diff --git a/madspace/include/madspace/driver/format.hpp b/madspace/include/madspace/driver/format.hpp index 7a709ebfa4..311fcafb58 100644 --- a/madspace/include/madspace/driver/format.hpp +++ b/madspace/include/madspace/driver/format.hpp @@ -8,13 +8,16 @@ namespace madspace { /// CPU time consumed by the current process, in microseconds. std::size_t cpu_time_microsec(); -/// Human-readable `wall_time_sec` and `cpu_time_sec`, e.g. `"1h 2m (85% CPU)"`. +/// Human-readable `wall_time_sec` and `cpu_time_sec`, formatted as +/// `"HH:MM:SS.cc wall, HH:MM:SS.cc cpu"`. std::string format_run_time(double wall_time_sec, double cpu_time_sec); -/// `value` with an SI magnitude suffix (`k`, `M`, ...), three significant digits. +/// `value`, rounded to an integer, with an SI magnitude suffix (`k`, `M`, ...) +/// and three significant digits. std::string format_si_prefix(double value); -/// `value` with its uncertainty `error`, e.g. `"1.23(4)"`. +/// `value` with its uncertainty `error`, e.g. `"1.234(45)"`. If `error` is not +/// smaller than `value`, the form `"value ± error"` is used. std::string format_with_error(double value, double error); -/// ASCII progress bar of `width` characters, `progress` in `[0, 1]`. +/// Progress bar of `width` Unicode block characters, `progress` in `[0, 1]`. std::string format_progress(double progress, int width); /** @@ -33,8 +36,9 @@ class PrettyBox { * @param title Text shown in the top border. * @param rows Number of content rows. * @param column_sizes Width of each column, in characters. - * @param offset Extra blank lines printed above the box. - * @param box_width Total box width, in characters. + * @param offset Extra lines between the end of the box and the cursor + * when it is redrawn. + * @param box_width Minimum box width; the last column is widened to it. */ PrettyBox( const std::string& title, diff --git a/madspace/include/madspace/driver/generator_data.hpp b/madspace/include/madspace/driver/generator_data.hpp index c089958006..73d9c30ec0 100644 --- a/madspace/include/madspace/driver/generator_data.hpp +++ b/madspace/include/madspace/driver/generator_data.hpp @@ -63,15 +63,16 @@ struct GeneratorConfig { std::size_t target_count = 10000; /// Damping factor of the VEGAS grid updates; see @ref VegasGridOptimizer. double vegas_damping = 0.2; - /// Fraction of the weight distribution's tail truncated when estimating - /// the unweighting efficiency. + /// Overweight tolerated when choosing the maximum weight, as a fraction of + /// the smaller of the target count and `freeze_max_weight_after`. double max_overweight_truncation = 0.01; - /// Number of survey events after which the maximum weight estimate is - /// frozen. + /// Expected number of unweighted events after which a channel's maximum + /// weight estimate is frozen. std::size_t freeze_max_weight_after = 10000; - /// Initial number of events per generation batch. + /// Initial number of events per VEGAS/channel-weight optimization batch. std::size_t start_batch_size = 1000; - /// Largest allowed number of events per generation batch. + /// Largest number of events per optimization batch (the batch size doubles + /// every iteration up to this value). std::size_t max_batch_size = 64000; /// Minimum number of VEGAS/channel-weight survey iterations. std::size_t survey_min_iters = 3; @@ -82,12 +83,12 @@ struct GeneratorConfig { /// Number of consecutive non-improving iterations tolerated before the /// survey stops optimizing. std::size_t optimization_patience = 3; - /// Minimum relative improvement counted as progress by - /// `optimization_patience`. + /// An iteration counts as progress for `optimization_patience` if the relative + /// standard deviation falls below this fraction of its best value so far. double optimization_threshold = 0.99; - /// Batch size used on a CPU device. + /// Number of events per job on a CPU device. std::size_t cpu_batch_size = 1000; - /// Batch size used on a GPU device. + /// Number of events per job on a GPU device. std::size_t gpu_batch_size = 64000; /// Progress-reporting verbosity. Verbosity verbosity = Verbosity::silent; @@ -96,11 +97,11 @@ struct GeneratorConfig { /// Thread count used to combine per-channel event files; `-1` for the /// hardware concurrency. int combine_thread_count = -1; - /// Minimum acceptable fraction of events surviving fiducial cuts before a - /// warning is raised. + /// A job repeats its batch until this fraction of its requested events + /// pass the cuts. double cut_efficiency_threshold = 0.7; - /// Maximum number of times a batch is resampled to reach - /// `cut_efficiency_threshold`. + /// Maximum number of repetitions per job to reach `cut_efficiency_threshold` + /// before an error is raised. std::size_t max_cut_repetitions = 100; /// Fraction of the target count below which the generator switches to /// finishing up the remaining events. @@ -175,22 +176,21 @@ struct GeneratorStatus { double mean_abs; /// Standard error of @ref mean_abs. double error_abs; - /// Relative standard deviation of the weights. + /// Relative standard deviation of the absolute weights. double rel_std_dev; /// Weighted events generated so far. std::size_t count; - /// Weighted events generated so far, restricted to the optimized phase. + /// Weighted events generated since the last reset of the event files. std::size_t count_opt; - /// Weighted events surviving fiducial cuts. + /// Weighted events surviving cuts. std::size_t count_after_cuts; - /// Weighted events surviving fiducial cuts, restricted to the optimized - /// phase. + /// Weighted events surviving cuts since the last reset of the event files. std::size_t count_after_cuts_opt; /// Unweighted events generated so far. double count_unweighted; /// Target unweighted event count; see @ref GeneratorConfig::target_count. std::size_t count_target; - /// Number of survey iterations run. + /// Number of optimization iterations run. std::size_t iterations; /// Number of optimization iterations in a row without enough improvement; /// the channel counts as optimized when this reaches @@ -211,23 +211,22 @@ struct Histogram { double min; /// Upper edge of the last bin. double max; - /// Summed weight per bin, including the under/overflow bins. + /// Weight per bin, including the under/overflow bins: raw sums in a + /// ChannelEventGenerator, normalized by EventGenerator::histograms(). std::vector bin_values; - /// Standard error per bin. + /// Standard error per bin (raw sums of squared weights in a + /// ChannelEventGenerator). std::vector bin_errors; }; -// Lightweight pending-work entry for EventGenerator::_ready_jobs, before start_jobs() -// has decided which context/how many sub-jobs to create from it. Kept separate from -// GeneratorBatchJob so a queue of pending batches doesn't carry the weight of every -// dispatched job's tensors and RNG bookkeeping. +// Pending-work entry of EventGenerator::_ready_jobs, before start_jobs() has decided +// which context and how many sub-jobs to create from it. struct ReadyJob { std::size_t channel_index; bool unweight; - // VEGAS batch: fixed size, start_jobs() dispatches it atomically in one go. - // Generation batch: events not yet dispatched -- start_jobs() decrements this in - // place as it creates sub-jobs, one device batch at a time, round-robining with - // other channels' ReadyJobs over however many calls it takes to reach zero. + // VEGAS batch: fixed size, dispatched in one go by start_jobs(). + // Generation batch: events not yet dispatched; start_jobs() decrements it as it + // creates sub-jobs, one device batch per visit. std::size_t batch_event_count; bool is_vegas_batch = false; }; @@ -235,16 +234,13 @@ struct ReadyJob { struct GeneratorBatchJob { std::size_t channel_index; bool unweight; - // Copied from the originating ReadyJob at dispatch time. For a VEGAS batch, this - // is the batch's fixed total, read by start_job()'s shrink-to-fit and by the - // done_event_count accounting in survey()/survey_deterministic(). For a - // generation batch it isn't read after dispatch -- generation sub-jobs always - // request a full device batch (see start_job()). + // Copied from the originating ReadyJob. For a VEGAS batch, this is the fixed + // total, read by start_job() and the done_event_count accounting in survey(). + // Not read for generation sub-jobs, which always request a full device batch. std::size_t batch_event_count; - // Total sub-jobs the batch was split into; only meaningful for VEGAS batches - // (see commit_generate_job()'s clear_events trigger). Generation batches are - // dispatched one sub-job per start_jobs() visit, so this isn't a full-batch count - // for them and isn't read. + // Total jobs (generate plus unweight stages) the VEGAS batch was split into; + // compared against the channel job count to detect its first committed job. + // Not meaningful for generation batches. std::size_t split_job_count; Tensor weights; // per-event bias, empty without a custom bias @@ -257,16 +253,15 @@ struct GeneratorBatchJob { std::size_t context_index; std::size_t job_id; double max_weight; - // Top-level seed plus job identity, used to derive this job's DerivedSeed(s) - // independently for generate vs unweight (see start_job()/submit_unweight_job()). + // Top-level seed plus job identity, from which start_job() and + // submit_unweight_job() derive their seeds. std::optional rng_seed; - // Per-channel dispatch sequence, assigned once at start_job() time. + // Per-channel dispatch sequence, assigned in start_job(). std::size_t rng_job_index = 0; bool rng_is_survey = false; std::size_t rng_survey_pass = 0; - // True for a VEGAS-grid-optimization batch, dispatched atomically by start_jobs() - // and shrunk to fit by start_job(). False for a steady-state generation batch, - // dispatched incrementally as device-sized sub-jobs (see ReadyJob). + // True for an optimization batch (dispatched at once, shrunk to fit by + // start_job()), false for a generation batch (device-sized sub-jobs). bool is_vegas_batch = false; // Nominal batch size start_job() requested; the job generates more raw points // when it repeats batches to get enough of them past the cuts. diff --git a/madspace/include/madspace/driver/io.hpp b/madspace/include/madspace/driver/io.hpp index 8366a34729..dcad99e0f6 100644 --- a/madspace/include/madspace/driver/io.hpp +++ b/madspace/include/madspace/driver/io.hpp @@ -20,15 +20,9 @@ Tensor load_tensor(TarReader& tar, const std::string& name); /// Append `tensor` as an npy file called `name` to the tar archive `tar`. void save_tensor(TarWriter& tar, const std::string& name, Tensor tensor); -// Peeks a .npy event file's own field descriptor (its header's "descr" -// entry) without needing to already know its layout -- returns the field -// names in declaration order (particle-level fields duplicated per particle -// slot with a "partN_" prefix, matching full_descr()'s own naming). Used -// where a file's actual layout has caller-optional fields the reader needs -// to auto-detect before committing to a DataLayout to open it with (eg -// LHE event records, which may or may not carry -// x1/fact_scale1/x2/fact_scale2/partial_weight_product depending on whether -// [generation] systematics was enabled when the file was written). +/// Field names, in order, from the "descr" entry of the header of the npy event +/// file `file`. Particle fields are repeated per particle with a "partN_" +/// prefix. Allows detecting optional fields before choosing a @ref DataLayout. std::vector peek_npy_field_names(const std::string& file); struct FieldLayout { @@ -229,8 +223,8 @@ class EventRecord { static constexpr int f_bias = 256; static constexpr int f_matrix_element = 512; - // `syst_weight_count` columns of systematic variation weights are appended - // when f_syst_weights is set; `syst_weight_ids` names them (rwgt_). + // With f_syst_weights, one column rwgt_ is added per entry of + // `syst_weight_ids`. static std::vector layout(int fields, const std::vector& syst_weight_ids = {}) { std::vector ret; @@ -504,10 +498,7 @@ class EventFile { void clear(); std::size_t particle_count() const { return _particle_count; } std::size_t event_count() const { return _event_count; } - // The DataLayout this file was actually constructed with -- lets a - // reader that only has an already-open EventFile& (not the layout that - // was used to open it) build a matching EventBuffer, rather than having - // to independently know/recompute the same layout itself. + // The DataLayout this file was opened with, for building a matching EventBuffer. const DataLayout& layout() const { return _layout; } ~EventFile(); diff --git a/madspace/include/madspace/driver/lhe_output.hpp b/madspace/include/madspace/driver/lhe_output.hpp index b224372195..665ad1f76f 100644 --- a/madspace/include/madspace/driver/lhe_output.hpp +++ b/madspace/include/madspace/driver/lhe_output.hpp @@ -17,17 +17,17 @@ namespace madspace { /** - * One `
` block of an LHE file's `` section. + * One block in the `
` section of an LHE file. * - * A free-form, generator-specific block of extra metadata, written verbatim - * under `name`. `escape_content` XML-escapes `content` before writing it. + * A free-form block of extra metadata, written as a tag `name` containing + * `content`. If `escape_content` is set, `content` is wrapped in CDATA. */ struct LHEHeader { /// The `
` block's tag name. std::string name; /// The block's text content. std::string content; - /// Whether to XML-escape `content` before writing it. + /// Whether to wrap `content` in a CDATA section. bool escape_content; }; @@ -38,7 +38,7 @@ struct LHEHeader { struct LHEProcess { /// Cross section of this subprocess, in pb. double cross_section; - /// Statistical uncertainty of `cross_section`. + /// Uncertainty of `cross_section`. double cross_section_error; /// Maximum event weight, used to unweight LHEF v1 weighted events. double max_weight; @@ -55,9 +55,9 @@ struct LHEMeta { int beam1_pdg_id, beam2_pdg_id; /// Energies of the two beams, in GeV. double beam1_energy, beam2_energy; - /// PDF group ids (LHAPDF's `PDFAuthors`) of the two beams. + /// PDF author group ids (LHEF `PDFGUP`) of the two beams. int beam1_pdf_authors, beam2_pdf_authors; - /// PDF set ids (LHAPDF's `LHAPDF ID`) of the two beams. + /// PDF set ids (LHEF `PDFSUP`) of the two beams. int beam1_pdf_id, beam2_pdf_id; /// LHEF `IDWTUP` event-weight convention. int weight_mode; @@ -95,6 +95,8 @@ struct LHEParticle { double lifetime; /// Cosine of the spin angle, or `9` if the spin is not specified. double spin; + /// MLM clustering scale in GeV, written for outgoing particles if + /// @ref LHEEvent::has_cluster_scales is set. double cluster_scale; }; @@ -152,20 +154,21 @@ struct LHEEvent { /// Whether the MLM clustering scales of the outgoing particles are written /// (`pt_clust_` attributes of ``). bool has_cluster_scales; - /// The renormalisation and factorisation scales, written as the standard - /// LHEF3 `` attributes. SCALUP carries only one number, and the two - /// codes do not agree on which: unwgt.f writes sqrt(max(q2fact)) there, - /// i.e. mu_F, while this writer puts mu_R. Emitting both explicitly means a - /// comparison does not have to guess. + /// Renormalisation scale, written as the `mur` attribute of ``. The + /// event `scale` (SCALUP) is written separately. double mu_r; + /// Factorisation scales of the two beams (`muf1`, `muf2`; `muf` is their + /// maximum). double mu_f1; double mu_f2; + /// Whether the `mur` and `muf*` attributes of `` are written. bool has_scales; /// Optional LHEF v3 weights (`` block); `rwgt_ids` and `rwgt` have /// the same length. std::vector rwgt_ids; std::vector rwgt; - /// Optional `` block. + /// Optional `` block; written only if `qcd_power` is nonnegative and + /// both beams took part. std::optional lo_info; /// Append this event's LHE text to `buffer`. @@ -191,30 +194,34 @@ class LHECompleter { int process_id; /// Decay topology of each diagram channel. std::vector topologies; - /// Final-state particle permutation of each channel. + /// Permutation of the external legs (incoming first) of each diagram of + /// each channel. nested_vector3 permutations; - /// Sampled-diagram index available to each channel. + /// Diagram index of each diagram of each channel. nested_vector2 diagram_indices; - /// Color-configuration indices available to each diagram. + /// Color-configuration indices of each diagram of each channel. nested_vector3 diagram_color_indices; /// Color-line assignment of each sampled color configuration. nested_vector2> color_flows; - /// Color representation (singlet, triplet, octet, ...) of each PDG id. + /// Color representation of each PDG id: 1 (singlet), 3, -3 or 8; ids + /// not listed count as singlets. std::unordered_map pdg_color_types; /// Per-particle helicity of each sampled helicity configuration. nested_vector2 helicities; - /// Per-particle PDG id of each sampled flavor configuration. + /// PDG ids per particle, for each flavor index and each of its options; + /// one option is chosen at random per event. nested_vector3 pdg_ids; - /// Per-diagram PDG override, indexed like `diagram_color_indices` - /// then by `Topology::Decay::flat_propagator_index`. Falls back to - /// `Topology::Decay::pdg_id` if empty. + /// Per-diagram PDG override, indexed by channel, diagram within the + /// channel and `Topology::Decay::flat_propagator_index`. Falls back to + /// `Topology::Decay::pdg_id` if missing. nested_vector3 diagram_propagator_pdgs; }; /// @param subproc_args Bookkeeping for every subprocess, indexed like /// `subprocess_index` in @ref complete_event_data. - /// @param bw_cutoff Number of widths within which a propagator is - /// sampled on its Breit-Wigner resonance. + /// @param bw_cutoff A resonance is written to an event only if its + /// invariant mass is within this many widths of its + /// mass. LHECompleter(const std::vector& subproc_args, double bw_cutoff); /** * Fill in `event`'s PDG ids, color flow, mother/daughter links, masses @@ -228,7 +235,7 @@ class LHECompleter { * @param color_index Sampled color configuration. * @param flavor_index Sampled flavor assignment. * @param helicity_index Sampled helicity configuration. - * @param rand_gen RNG stream, used to sample resonance masses. + * @param rand_gen RNG stream, used to pick the flavor option. */ void complete_event_data( LHEEvent& event, @@ -239,7 +246,7 @@ class LHECompleter { int helicity_index, MixMaxRandom& rand_gen ); - /// Largest particle count over every subprocess. + /// Largest particle count over every subprocess, including resonances. std::size_t max_particle_count() const { return _max_particle_count; } /// Serialize this completer's bookkeeping to `file`. void save(const std::string& file) const; @@ -252,8 +259,7 @@ class LHECompleter { std::size_t color_offset, pdg_id_offset, helicity_offset, mass_offset; std::size_t particle_count, color_count, flavor_count; std::size_t diagram_count, helicity_count; - // 2 for a collision, 1 for a decay. Decides which leading particles are - // written as initial state and what the outgoing ones point at. + // 2 for a collision, 1 for a decay. std::size_t incoming_count; }; struct PropagatorData { diff --git a/madspace/include/madspace/driver/madnis_training.hpp b/madspace/include/madspace/driver/madnis_training.hpp index 8d274c0ab6..6e9f510de6 100644 --- a/madspace/include/madspace/driver/madnis_training.hpp +++ b/madspace/include/madspace/driver/madnis_training.hpp @@ -46,13 +46,16 @@ class MadnisTraining { double learning_rate = 1e-3; /// Number of training batches. std::size_t batches = 1000; - /// Batches between two status-history entries. + /// Batches between two status-history entries; also the length of + /// the window averaged by @ref average_loss. std::size_t log_interval = 100; /// Number of integration-history entries kept per channel. std::size_t integration_history_length = 1000; /// Batches between two channel-dropping checks. std::size_t channel_dropping_interval = 100; - /// Minimum integral fraction below which a channel is dropped. + /// Channels are dropped, smallest integral first, while their + /// cumulative integral fraction stays below this (scaled by + /// `min(1000 / batch, 1)`). double channel_dropping_threshold = 0.01; /// Generator batch size on a CPU device. std::size_t cpu_generator_batch_size = 1000; @@ -62,9 +65,10 @@ class MadnisTraining { std::size_t gpu_generator_batch_granularity = 1000; /// Multiple of the training batch size kept buffered per channel. std::size_t generator_target_size_factor = 32; - /// Constant added to every channel's requested batch size. + /// Constant added to the total training batch size, which is split + /// over the active channels. std::size_t batch_size_offset = 512; - /// Minimum training-batch events requested per active channel. + /// Training-batch events added to the total per active channel. std::size_t batch_size_per_channel = 128; /// Fraction of the training batch drawn uniformly across channels /// rather than proportionally to their integral. @@ -77,9 +81,9 @@ class MadnisTraining { double adam_beta2 = 0.999; /// Adam numerical-stability constant. double adam_eps = 1e-8; - /// Adam L2 weight-decay coefficient. + /// Adam decoupled weight-decay coefficient. double adam_weight_decay = 0.0; - /// Maximum gradient norm; `0` disables clipping. + /// Maximum gradient norm per sqrt(parameter count); `0` disables clipping. double grad_clip_threshold = 0.0; /// Maximum number of samples kept in the replay buffer per channel; /// `0` disables buffering. @@ -93,10 +97,10 @@ class MadnisTraining { std::size_t buffer_skip_batches = 1000; /// Quantile used to estimate the max weight for buffer unweighting. double buffer_unweighting_quantile = 0.99; - /// Fraction of training spent with the channel-weight network frozen, - /// before it starts training too. + /// Fraction of training at the end during which the channel-weight + /// network is frozen. double fixed_cwnet_fraction = 0.33; - /// Softclip threshold applied to the channel weights; `0` disables it. + /// Soft-clipping threshold on the loss contribution; `0` disables it. double softclip_threshold = 0.0; /// Number of channel weights kept per event when compressing the /// multi-channel weight loss. @@ -304,13 +308,9 @@ class MultiMadnisTraining { * @param verbosity Progress-reporting verbosity. * @param status_file Optional status file progress is * periodically written to. - * @param seed Top-level run seed (reusing the run card's - * own seed, also used by - * `build_event_generator()`); each - * subprocess's `MadnisTraining` gets a - * `channel_index_offset` slice of this seed's - * stream so their derived seeds don't - * collide. + * @param seed Top-level run seed; each subprocess's + * `MadnisTraining` gets a `channel_index_offset` + * slice so their derived seeds don't collide. */ MultiMadnisTraining( ContextPtr generator_context, diff --git a/madspace/include/madspace/driver/random.hpp b/madspace/include/madspace/driver/random.hpp index 821d054a25..e74ddca7f5 100644 --- a/madspace/include/madspace/driver/random.hpp +++ b/madspace/include/madspace/driver/random.hpp @@ -47,10 +47,9 @@ struct DerivedSeed { /// Cached MIXMAX state after applying the 64 global-run-seed bits /// (`seed_parts[0..1]`, i.e. effective seed bits `[64, 128)`). These bits do -/// not change within a run, so every @ref MixMaxRandom shares this cache and -/// only recomputes it when the run seed actually differs, for example when -/// madspace is driven as a library across several runs. The per-call bits -/// `[0, 64)` are applied on top by @ref MixMaxRandom. +/// not change within a run, so each @ref MixMaxRandom keeps this cache and +/// only recomputes it when the run seed differs from the cached one. The +/// per-call bits `[0, 64)` are applied on top by @ref MixMaxRandom. struct RunSeedSkip { std::array state; std::uint64_t run_seed = 0; diff --git a/madspace/include/madspace/driver/status_file.hpp b/madspace/include/madspace/driver/status_file.hpp index 4a126cdbf5..5585a42ef0 100644 --- a/madspace/include/madspace/driver/status_file.hpp +++ b/madspace/include/madspace/driver/status_file.hpp @@ -10,8 +10,8 @@ namespace madspace { /** * Rate-limited JSON status file, merged from possibly multiple sources. * - * Lets several parts of a run (or several processes) report progress into - * the same file without flooding the disk: @ref write merges its argument + * Lets several parts of a run report progress into the same file without + * flooding the disk: @ref write merges its argument * into the accumulated content and only actually rewrites the file at most * every @p min_interval_sec, unless forced. */ diff --git a/madspace/include/madspace/driver/systematics.hpp b/madspace/include/madspace/driver/systematics.hpp index 25321c57c0..c7ab6a33ff 100644 --- a/madspace/include/madspace/driver/systematics.hpp +++ b/madspace/include/madspace/driver/systematics.hpp @@ -159,8 +159,8 @@ class SystematicsCalculator { /** * @param config The scale and PDF variations to compute. * @param subproc_args Per-subprocess bookkeeping needed to reweight - * its events; indexed like the subprocess index - * passed to @ref compute. + * its events; indexed by the events' + * subprocess index. * @param nominal_pdf The PDF the events were generated with; * `nullopt` for leptonic beams. * @param nominal_alpha_s The `alpha_s` grid the events were generated @@ -202,8 +202,8 @@ class SystematicsCalculator { std::size_t weight_count() const { return _variations.size(); } /// LHEF `` ids, in the order of @ref variations. std::vector weight_ids() const; - /// PDF members actually used (the nominal set's member 0 first, when it - /// is part of a group). + /// PDF members actually used: the configured ones minus those ignored + /// with a warning. The nominal member is not listed. const std::vector& members() const { return _members; } /// Warnings emitted while building the variation list. const std::vector& warnings() const { return _warnings; } @@ -244,11 +244,11 @@ class SystematicsCalculator { * * @param error_type LHAPDF `ErrorType` (`replicas`, `hessian`, ...). * @param central Central value; used when member `0` is absent - * from `member_values`. + * from `member_values` (not used for `replicas`). * @param member_values `(member number, value)` pairs; member `0` is * the central member. - * @return `(central, up, down)`; `NaN` entries when they cannot be - * computed. + * @return `(central, up, down)`; for `replicas` the first entry is the + * replica mean. `NaN` entries when they cannot be computed. */ static std::tuple pdf_uncertainty( const std::string& error_type, diff --git a/madspace/include/madspace/driver/tensor.hpp b/madspace/include/madspace/driver/tensor.hpp index e9368d607c..61f55cdebe 100644 --- a/madspace/include/madspace/driver/tensor.hpp +++ b/madspace/include/madspace/driver/tensor.hpp @@ -230,7 +230,7 @@ using TensorVec = std::vector; enum class DeviceType { cpu, cuda, hip }; /// Hint passed to @ref Device::allocate describing how a tensor will be used, -/// so the allocator can reuse scratch buffers or skip a zero-fill. +/// so the allocator can pool scratch buffers; gradient hints request zeroed storage. enum class AllocHint { normal, output, @@ -242,7 +242,7 @@ enum class AllocHint { global_grad, }; -/// Whether newly allocated storage for `hint` must be zero-initialized. +/// Whether newly allocated storage for `hint` is zero-initialized (gradients). inline bool needs_zero_init(AllocHint hint) { return hint == AllocHint::input_grad || hint == AllocHint::local_grad || hint == AllocHint::global_grad; @@ -620,7 +620,7 @@ class Tensor { storage()->stream = stream; } } - /// The single integer value of a scalar `DataType::batch_sizes` tensor. + /// The value of a single-element integer tensor. std::size_t index_value() const { check_impl(); if (impl->batch_sizes.size() > 0) { @@ -683,16 +683,17 @@ class Tensor { Tensor unsqueeze(std::size_t axis) const; /// Broadcasts size-1 dimensions to `shape`, without copying. Tensor expand(const Sizes& shape) const; - /// A view with a different `shape` over the same elements. + /// A view with a different `shape` over the same elements; requires a + /// contiguous tensor. Tensor reshape(const Sizes& shape) const; - /// Splits dimension `axis` into two, the second of size `factor`. + /// Splits dimension `axis` into two, the first of size `factor`. Tensor factor_dim(std::size_t axis, std::size_t factor); /// Splits along the batch dimension and reshapes each piece. std::vector split_and_reshape(const std::vector& shapes) const; template - /// A copy of this tensor on the CPU, allocated through `device`, or - /// `*this` if it is already there. + /// A copy of this tensor on the CPU, made using `device`, or `*this` if it + /// is already there. Tensor cpu(const D& device) const { check_impl(); if (impl->device == cpu_device()) { diff --git a/madspace/include/madspace/driver/thread_pool.hpp b/madspace/include/madspace/driver/thread_pool.hpp index 9b41ea1cdf..3de7c47328 100644 --- a/madspace/include/madspace/driver/thread_pool.hpp +++ b/madspace/include/madspace/driver/thread_pool.hpp @@ -50,8 +50,6 @@ class ThreadPool { std::vector _done_buffer; std::size_t _busy_threads = 0; // The first exception thrown by a job, rethrown by wait()/wait_multiple(). - // Without it an exception escaping a job would leave std::thread with no - // handler, i.e. terminate the process. std::exception_ptr _exception; std::size_t _listener_id = 0; std::unordered_map> _listeners; diff --git a/madspace/include/madspace/driver/vegas_optimizer.hpp b/madspace/include/madspace/driver/vegas_optimizer.hpp index 41685dd502..ba53dc4992 100644 --- a/madspace/include/madspace/driver/vegas_optimizer.hpp +++ b/madspace/include/madspace/driver/vegas_optimizer.hpp @@ -9,17 +9,17 @@ namespace madspace { /** * Refines a @ref VegasMapping grid from accumulated bin statistics. * - * The training-time counterpart of @ref VegasHistogram — @ref add_data feeds - * in the per-bin weight sums accumulated during a warm-up run, and @ref - * optimize rewrites the `grid_name` global on every context in `contexts` - * with the refined grid. + * Counterpart of @ref VegasHistogram: @ref add_data feeds in the per-bin + * weight sums and counts it accumulates, and @ref optimize rewrites the + * `grid_name` global on every context in `contexts` with the refined grid. */ class VegasGridOptimizer { public: /** * @param contexts Contexts sharing the grid to refine. * @param grid_name Global name of the VEGAS grid. - * @param damping Damping factor limiting the size of a single update. + * @param damping Exponent of the damping applied to the normalized bin + * values; smaller values damp more strongly. */ VegasGridOptimizer( const std::vector& contexts, @@ -27,8 +27,8 @@ class VegasGridOptimizer { double damping ) : _contexts(contexts), _grid_name(grid_name), _damping(damping) {} - /// Accumulate one batch of per-bin `weights` and `inputs`; see @ref - /// VegasHistogram. + /// Accumulate one batch of per-bin weight sums (`values`) and per-bin + /// counts (`counts`) with shape [1, dims, bins]; see @ref VegasHistogram. void add_data(Tensor weights, Tensor inputs); /// Refine the grid from the accumulated data and write it back to every /// context. diff --git a/madspace/include/madspace/phasespace/base.hpp b/madspace/include/madspace/phasespace/base.hpp index 889d8759f4..c2d64a12df 100644 --- a/madspace/include/madspace/phasespace/base.hpp +++ b/madspace/include/madspace/phasespace/base.hpp @@ -30,7 +30,7 @@ namespace madspace { * A `Mapping` records one such @f$G@f$ as a pair of compute-graph builders. * `build_forward` maps the **inputs** (the unit-hypercube numbers) to the * **outputs** (momenta and invariants). `build_inverse` maps the outputs back - * to the inputs. Both also return a `weight`, the Jacobian @f$1/g@f$ of the + * to the inputs. Both also return a `det`, the Jacobian @f$1/g@f$ of the * transformation. **Conditions** are external quantities the mapping needs but * does not transform, such as masses, the collision energy or cut boundaries. * They are the same for the forward and inverse direction. diff --git a/madspace/include/madspace/phasespace/batch_sampler.hpp b/madspace/include/madspace/phasespace/batch_sampler.hpp index 7d2cc8981a..55a9db7c47 100644 --- a/madspace/include/madspace/phasespace/batch_sampler.hpp +++ b/madspace/include/madspace/phasespace/batch_sampler.hpp @@ -5,21 +5,20 @@ namespace madspace { /** - * Concatenates the per-channel tensors of a multi-channel batch. + * Draws random batches from per-channel buffers. * - * Given one tensor set per channel and the per-channel batch sizes, it packs - * them into a single contiguous batch (the inverse of the split done inside - * @ref MultiChannelMapping). Used to assemble a mixed batch for the shared - * integrand evaluation (see Sec. 3.1 of [1]). + * For each channel, picks `batch_sizes[i]` rows at random indices (with + * replacement) of that channel's buffered tensors. Used to draw MadNIS training + * batches from the replay buffer. * * `batch` is the leading batch dimension. `i` indexes the channels. * * **Arguments** - * - `channel_in_` – the per-channel input tensors. - * - `batch_sizes` – the number of entries in each channel. + * - `channel_in_` – the buffered tensors of channel `i`. + * - `batch_sizes` – the number of rows to draw for each channel. * * **Returns** - * - `channel_out_` – the concatenated tensors. + * - `channel_out_` – the drawn rows, one tensor per input tensor. * * **References** * - [1] T. Heimel, O. Mattelaer, R. Winterhalder, "MadSpace", diff --git a/madspace/include/madspace/phasespace/channel_weight_network.hpp b/madspace/include/madspace/phasespace/channel_weight_network.hpp index 23b14cafda..70c41029a4 100644 --- a/madspace/include/madspace/phasespace/channel_weight_network.hpp +++ b/madspace/include/madspace/phasespace/channel_weight_network.hpp @@ -8,7 +8,7 @@ namespace madspace { /** * Turns event momenta into invariant features for @ref ChannelWeightNetwork. * - * Computes @f$(p_\mathrm{T}, \eta, \phi)@f$ for each particle together with the + * Computes @f$(p_\mathrm{T}, \eta, \phi)@f$ for each final-state particle with the * momentum fractions, a representation the channel-weight network can consume * directly [1]. * @@ -75,8 +75,8 @@ class ChannelWeightNetwork : public FunctionGenerator { * @param layers Number of @ref MLP layers. * @param activation @ref MLP activation. * @param prefix Prefix for the trainable global names. - * @param include_preprocessing If true, apply @ref MomentumPreprocessing - * to the input first. + * @param include_preprocessing Currently unused: the network always takes + * the preprocessed features as `input`. */ ChannelWeightNetwork( std::size_t channel_count, diff --git a/madspace/include/madspace/phasespace/channel_weights.hpp b/madspace/include/madspace/phasespace/channel_weights.hpp index e058bad6d5..b9524ce38e 100644 --- a/madspace/include/madspace/phasespace/channel_weights.hpp +++ b/madspace/include/madspace/phasespace/channel_weights.hpp @@ -36,10 +36,10 @@ class PropagatorChannelWeights : public FunctionGenerator { public: /** * @param topologies One @ref Topology per channel. - * @param permutations Per channel, the outgoing-particle permutations to - * average the weight over. - * @param channel_indices Per channel, the global channel indices its weight - * contributes to. + * @param permutations Per topology, the outgoing-particle permutations; + * each one is a separate output channel. + * @param channel_indices Per topology, the output channel index of each of + * its permutations. */ PropagatorChannelWeights( const std::vector& topologies, @@ -87,7 +87,8 @@ class SubchannelWeights : public FunctionGenerator { * @param topologies Per channel, its list of on-shell sub-channel * @ref Topology "topologies". * @param permutations Per channel, the outgoing-particle permutations. - * @param channel_indices Per channel, the global channel indices. + * @param channel_indices Per channel, the input channel index of each + * permutation. */ SubchannelWeights( const nested_vector2& topologies, @@ -95,7 +96,7 @@ class SubchannelWeights : public FunctionGenerator { const nested_vector2& channel_indices ); - /// Number of input channels. + /// Number of output channels (the sub-channels). std::size_t channel_count() const { return _channel_indices.size(); } private: diff --git a/madspace/include/madspace/phasespace/chili.hpp b/madspace/include/madspace/phasespace/chili.hpp index 396c5ea528..77f9429e54 100644 --- a/madspace/include/madspace/phasespace/chili.hpp +++ b/madspace/include/madspace/phasespace/chili.hpp @@ -49,7 +49,7 @@ namespace madspace { * - `momentum_i` – `float`, shape `(batch, 4)` – the `n_particles + 2` momenta * (incoming beams first, then outgoing). * - * In addition every mapping returns a `weight` (`float`, shape `(batch,)`), the + * In addition every mapping returns a `det` (`float`, shape `(batch,)`), the * Jacobian of the transformation. * * **References** @@ -66,10 +66,11 @@ class ChiliMapping : public Mapping { public: /** * @param n_particles Number of outgoing particles. - * @param y_max Per-outgoing-particle maximum rapidity; empty disables - * the rapidity cut. See @ref Cuts. - * @param pt_min Per-outgoing-particle minimum transverse momentum; - * empty disables the cut. See @ref Cuts. + * @param y_max Per-outgoing-particle maximum rapidity, one entry per + * particle; infinity disables the cut. See @ref Cuts. + * @param pt_min Per-outgoing-particle minimum transverse momentum, one + * entry per particle (the last is unused); 0 disables + * the cut. See @ref Cuts. */ ChiliMapping( std::size_t n_particles, diff --git a/madspace/include/madspace/phasespace/color_ordered_mapping.hpp b/madspace/include/madspace/phasespace/color_ordered_mapping.hpp index a59e4d11d3..d5519ef4e6 100644 --- a/madspace/include/madspace/phasespace/color_ordered_mapping.hpp +++ b/madspace/include/madspace/phasespace/color_ordered_mapping.hpp @@ -19,7 +19,8 @@ namespace madspace { * blocks (@ref TwoToThreeParticleScattering) laid out along a color-ordered * permutation of the particles, peeling one on-shell momentum off at a time * from each beam, with a @ref TwoToTwoParticleScattering or @ref DoubleT - * central block joining the two sides [1]. It encodes no specific diagram + * central block joining the two sides [1] (no central block if all outgoing + * particles sit on one side). It encodes no specific diagram * topology. The `discrete_i` inputs select, per @f$2 \to 3@f$ peel, which of * the two branches to take. See Sec. 2.2.5 of [1], following [2]. * @@ -27,22 +28,22 @@ namespace madspace { * particles and `n = n_out + 2` the total. * * **Inputs** - * - `random_i` – `float`, shape `(batch,)` – the continuous random numbers + * - `random_i` - `float`, shape `(batch,)` – the continuous random numbers * (`random_dim()` of them). - * - `discrete_i` – `int`, shape `(batch,)` – the two-solution choices + * - `discrete_i` - `int`, shape `(batch,)` – the two-solution choices * (`discrete_dim()` of them). * * **Conditions** - * - `com_energy` – `float`, shape `(batch,)` – total collision energy. - * - `mass_i` – `float`, shape `(batch,)` – the `n_out` outgoing masses. - * - `x1`, `x2` – `float`, shape `(batch,)` – beam momentum fractions. Present + * - `com_energy` - `float`, shape `(batch,)` – total collision energy. + * - `mass_i` - `float`, shape `(batch,)` – the `n_out` outgoing masses. + * - `x1`, `x2` - `float`, shape `(batch,)` – beam momentum fractions. Present * only when a rapidity bound is used (see the constructor). * * **Outputs** - * - `momentum_i` – `float`, shape `(batch, 4)` – the `n` momenta (incoming + * - `momentum_i` - `float`, shape `(batch, 4)` – the `n` momenta (incoming * beams first, then outgoing). * - * In addition every mapping returns a `weight` (`float`, shape `(batch,)`), the + * In addition every mapping returns a `det` (`float`, shape `(batch,)`), the * Jacobian of the transformation. * * **References** @@ -187,9 +188,8 @@ class ColorOrderedMapping : public Mapping { std::vector _set2; std::size_t _n_out; std::size_t _random_dim; - // Number of discrete two-solution choices (one per 2->3 peel). These are - // supplied/recovered as a separate batch_int channel, not through the - // continuous random_dim() block (opt-in r_disc). + // Number of discrete two-solution choices (one per 2->3 peel). They are + // separate batch_int inputs after the continuous randoms. std::size_t _discrete_dim; // True iff exactly one of (set1, set2) has size 1 (and the other >= 2). // In that case the central block is DoubleT instead of 2->2. diff --git a/madspace/include/madspace/phasespace/cross_section.hpp b/madspace/include/madspace/phasespace/cross_section.hpp index d30b7629c8..683797d6ed 100644 --- a/madspace/include/madspace/phasespace/cross_section.hpp +++ b/madspace/include/madspace/phasespace/cross_section.hpp @@ -18,12 +18,13 @@ namespace madspace { * With @p decay the generator instead describes the decay of a single incoming * particle. The differential rate is then @f$|M|^2 / (2 M)@f$ with * @f$M@f$ = @p cm_energy, in GeV, rather than a hadronic cross section in pb. - * There are no beams, hence no momentum fractions and no PDFs. + * There are no beams, hence no PDFs; declared `x1`, `x2` inputs are ignored. * * `batch` is the leading batch dimension. * * **Arguments** - * - the arguments of @p matrix_element, plus + * - the arguments of @p matrix_element (except `alpha_s` unless it is + * cached), plus * - `x1`, `x2` – `float`, shape `(batch,)` – parton momentum fractions (when * @p input_momentum_fraction is true). * - `pdf_id` – `int`, shape `(batch,)` – selected flavour combination. @@ -49,7 +50,8 @@ class DifferentialCrossSection : public FunctionGenerator { /** * @param matrix_element The squared matrix element. - * @param cm_energy Total collision energy. + * @param cm_energy Total collision energy (the mass of the + * decaying particle with @p decay). * @param running_coupling Optional @f$\alpha_s@f$ source. * @param energy_scale An @ref EnergyScale, a `CachedScale` tag, * or nothing. diff --git a/madspace/include/madspace/phasespace/discrete_flow.hpp b/madspace/include/madspace/phasespace/discrete_flow.hpp index a4affa9bde..79708562dc 100644 --- a/madspace/include/madspace/phasespace/discrete_flow.hpp +++ b/madspace/include/madspace/phasespace/discrete_flow.hpp @@ -10,25 +10,26 @@ namespace madspace { * * Like @ref DiscreteSampler, but the categorical distribution for each discrete * dimension is predicted by an @ref MLP subnetwork from the earlier choices and - * an optional conditioning input [1]. The subnetwork weights are compute-graph - * globals named after @p prefix; call `initialize_globals(context)` once before - * use. + * an optional conditioning input [1]. Without conditioning input, the first + * dimension has a learned categorical distribution instead. The subnetwork + * weights are compute-graph globals named after @p prefix; call + * `initialize_globals(context)` once before use. * * `batch` is the leading batch dimension. `d` indexes the discrete dimensions. * * **Inputs** - * - `random_d` – `float`, shape `(batch,)` – one uniform number per dimension. + * - `random_{d}` – `float`, shape `(batch,)` – one uniform number per dimension. * * **Conditions** * - `condition` – `float`, shape `(batch, condition_dim)` – conditioning input. * Present only when @p condition_dim is nonzero. - * - `prior_d` – `float`, shape `(batch, option_counts[d])` – prior category + * - `prior_{d}` – `float`, shape `(batch, option_counts[d])` – prior category * weights. Present only for the dimensions in @p dims_with_prior. * * **Outputs** - * - `index_d` – `int`, shape `(batch,)` – the chosen category per dimension. + * - `index_{d}` – `int`, shape `(batch,)` – the chosen category per dimension. * - * In addition every mapping returns a `weight` (`float`, shape `(batch,)`), the + * In addition every mapping returns a `det` (`float`, shape `(batch,)`), the * Jacobian of the transformation. * * **References** @@ -40,7 +41,7 @@ class DiscreteFlow : public Mapping { /** * @param option_counts Number of categories for each discrete dimension. * @param prefix Prefix for the trainable global names. - * @param dims_with_prior Dimensions conditioned on a `prior_d` input. + * @param dims_with_prior Dimensions conditioned on a `prior_{d}` input. * @param condition_dim Width of the conditioning input; `0` for none. * @param subnet_hidden_dim Hidden width of the @ref MLP subnetworks. * @param subnet_layers Number of layers in the subnetworks. diff --git a/madspace/include/madspace/phasespace/discrete_sampler.hpp b/madspace/include/madspace/phasespace/discrete_sampler.hpp index a4b40ee18a..5c0133ad97 100644 --- a/madspace/include/madspace/phasespace/discrete_sampler.hpp +++ b/madspace/include/madspace/phasespace/discrete_sampler.hpp @@ -8,20 +8,20 @@ namespace madspace { /** * Per-category weight accumulator that adapts a @ref DiscreteSampler. * - * During the warm-up run it sums the sample weights for each category of each - * discrete dimension. The accumulated `values_d` and `counts_d` are then used + * Sums the sample weights for each category of each discrete dimension. The + * accumulated `values{d}` and `counts{d}` are then used * to refine the learned categorical distributions (Sec. 3.2.2 of [1]). * * `batch` is the leading batch dimension. `d` indexes the discrete dimensions. * * **Arguments** - * - `index_d` – `int`, shape `(batch,)` – the chosen category per dimension. + * - `index{d}` – `int`, shape `(batch,)` – the chosen category per dimension. * - `weight` – `float`, shape `(batch,)` – the per-sample weights. * * **Returns** - * - `values_d` – `float`, shape `(option_counts[d],)` – summed weight per + * - `values{d}` – `float`, shape `(option_counts[d],)` – summed weight per * category. - * - `counts_d` – `int`, shape `(option_counts[d],)` – sample count per category. + * - `counts{d}` – `int`, shape `(option_counts[d],)` – sample count per category. * * **References** * - [1] T. Heimel, O. Mattelaer, R. Winterhalder, "MadSpace", @@ -46,22 +46,22 @@ class DiscreteHistogram : public FunctionGenerator { * Maps one uniform random number per discrete dimension to a category index, * using a learned categorical distribution per dimension [1]. The * probabilities are compute-graph globals named after @p prefix; call - * `initialize_globals(context)` once before use. A dimension listed in - * @p dims_with_prior is instead conditioned on a `prior_i` input. + * `initialize_globals(context)` once before use. For a dimension listed in + * @p dims_with_prior the probabilities are multiplied by a `prior{d}` input. * * `batch` is the leading batch dimension. `d` indexes the discrete dimensions. * * **Inputs** - * - `random_i` – `float`, shape `(batch,)` – one uniform number per dimension. + * - `random{d}` – `float`, shape `(batch,)` – one uniform number per dimension. * * **Conditions** - * - `prior_d` – `float`, shape `(batch, option_counts[d])` – prior category + * - `prior{d}` – `float`, shape `(batch, option_counts[d])` – prior category * weights. Present only for the dimensions in @p dims_with_prior. * * **Outputs** - * - `index_i` – `int`, shape `(batch,)` – the chosen category per dimension. + * - `index{d}` – `int`, shape `(batch,)` – the chosen category per dimension. * - * In addition every mapping returns a `weight` (`float`, shape `(batch,)`), the + * In addition every mapping returns a `det` (`float`, shape `(batch,)`), the * Jacobian of the transformation. * * **References** @@ -73,8 +73,8 @@ class DiscreteSampler : public Mapping { /** * @param option_counts Number of categories for each discrete dimension. * @param prefix Prefix for the probability global names. - * @param dims_with_prior Dimensions conditioned on a `prior_i` input - * rather than a learned global. + * @param dims_with_prior Dimensions whose learned probabilities are + * multiplied by a `prior{d}` input. */ DiscreteSampler( const std::vector& option_counts, @@ -111,6 +111,7 @@ class DiscreteSampler : public Mapping { std::vector _prob_names; }; +/// Define the global @p name and fill it with uniform probabilities. void initialize_uniform_probs( ContextPtr context, const std::string& name, std::size_t option_count ); diff --git a/madspace/include/madspace/phasespace/flow.hpp b/madspace/include/madspace/phasespace/flow.hpp index 94eeaa3f82..a8ad942775 100644 --- a/madspace/include/madspace/phasespace/flow.hpp +++ b/madspace/include/madspace/phasespace/flow.hpp @@ -30,7 +30,7 @@ namespace madspace { * **Outputs** * - `data` – `float`, shape `(batch, input_dim)` – the transformed coordinates. * - * In addition every mapping returns a `weight` (`float`, shape `(batch,)`), the + * In addition every mapping returns a `det` (`float`, shape `(batch,)`), the * Jacobian of the transformation. * * **References** diff --git a/madspace/include/madspace/phasespace/histograms.hpp b/madspace/include/madspace/phasespace/histograms.hpp index a88c06d741..0222d904d0 100644 --- a/madspace/include/madspace/phasespace/histograms.hpp +++ b/madspace/include/madspace/phasespace/histograms.hpp @@ -22,8 +22,8 @@ namespace madspace { * - `momenta` – `float`, shape `(batch, n, 4)` – the event momenta. * * **Returns** - * - `values_i` – `float`, shape `(bin_count + 2,)` – summed weight per bin. - * - `square_values_i` – `float`, shape `(bin_count + 2,)` – summed squared + * - `values{i}` – `float`, shape `(bin_count + 2,)` – summed weight per bin. + * - `square_values{i}` – `float`, shape `(bin_count + 2,)` – summed squared * weight per bin. * * **References** @@ -70,7 +70,7 @@ class ObservableHistograms : public FunctionGenerator { * - `momenta` – `float`, shape `(batch, n, 4)` – the event momenta. * * **Returns** - * - `value_i` – `float`, shape `(batch,)` – the value of observable `i`. An + * - `value{i}` – `float`, shape `(batch,)` – the value of observable `i`. An * event-level observable returning a vector keeps its own shape. * * **References** diff --git a/madspace/include/madspace/phasespace/integrand.hpp b/madspace/include/madspace/phasespace/integrand.hpp index 0bab9ef189..bf3c24b5f1 100644 --- a/madspace/include/madspace/phasespace/integrand.hpp +++ b/madspace/include/madspace/phasespace/integrand.hpp @@ -43,7 +43,8 @@ namespace madspace { * - `color_index`, `helicity_index`, `diagram_index`, `flavor_index` – `int`, * shape `(batch,)` – the sampled discrete quantities. * - `ren_scale`, `alpha_qcd`, `x1`, `x2`, `fact_scale1`, `fact_scale2` – - * `float`, shape `(batch,)` – scales and momentum fractions. + * `float`, shape `(batch,)` – scales and momentum fractions (`x1`, `x2` and + * the factorisation scales only for beams with a PDF). * - `random` – `float`, shape `(batch, mapping.random_dim())` – the underlying * random numbers. * - `bias` – `float`, shape `(batch,)` – the event bias, present only with a @@ -98,9 +99,10 @@ class Integrand : public FunctionGenerator { * combinations. * @param pdf_grid Optional shared @ref PdfGrid or * @ref CustomPdf. - * @param running_coupling Optional shared - * @ref RunningCoupling. - * @param energy_scale Optional shared @ref EnergyScale. + * @param running_coupling Shared @ref RunningCoupling + * (dereferenced unconditionally). + * @param energy_scale Shared @ref EnergyScale + * (dereferenced unconditionally). * @param prop_chan_weights Optional * @ref PropagatorChannelWeights. * @param subchan_weights Optional @ref SubchannelWeights. @@ -109,30 +111,36 @@ class Integrand : public FunctionGenerator { * @param first_chan_weight_remap Remap applied to the channel * weights before the network. * @param first_remapped_chan_count Channel count after that remap. - * @param second_chan_weight_remap Remap applied after the network. + * @param second_chan_weight_remap Remap applied before the network, + * after the subchannel weights. * @param second_remapped_chan_count Channel count after that remap. * @param madnis_training If true, return the quantities * needed for the @ref MadnisLoss * instead of a finished event. - * @param drop_cuts_and_rescale If true, ignore the cut mask and - * rescale the weight accordingly. - * @param partial_weights If true, emit the per-factor - * weights for buffered unweighting. + * @param drop_cuts_and_rescale If true, return only the events + * that pass the cuts, and rescale + * adaptive_prob by the acceptance. + * @param partial_weights If true, emit the product of the + * beam densities (partial_weight_product) + * for the systematics. * Ignored under MLM pdf * reweighting, whose densities are * not at the factorisation scale. * @param channel_indices Global indices of this * integrand's channels. - * @param active_flavors Flavour indices this integrand - * covers. - * @param flavor_remap Map from flavour index to - * @p diff_xs index. + * @param active_flavors For each permutation channel, + * the flavour indices it covers. + * @param flavor_remap Per flavour index, the flavour + * index passed to @p diff_xs. * @param flavor_factors Per-flavour multiplicative * weight factors. * @param flavor_mirror Per-flavour beam-swap flags. - * @param flavor_diff_xs_indices Per-flavour @p diff_xs indices. - * @param flavor_subproc_indices Per-flavour sub-process indices. - * @param flavor_per_subproc_remap Per-sub-process flavour remap. + * @param flavor_diff_xs_indices Per flavour, the index into + * @p diff_xs. + * @param flavor_subproc_indices Per-flavour sub-process indices + * (output subprocess_index). + * @param flavor_per_subproc_remap Per-flavour index written as + * flavor_index in the output. * @param compressed_channel_weight_count Channel-weight entries kept per * event. * @param custom_bias Optional user-supplied event @@ -240,10 +248,10 @@ class Integrand : public FunctionGenerator { const std::optional& chan_weight_net() const { return _chan_weight_net; } - /// Number of continuous random numbers the integrand consumes. + /// Number of random numbers per event, including those for discrete choices. const std::size_t random_dim() const { return _random_dim; } - /// The continuous latent dimensions and, per dimension, whether it is - /// discrete. + /// The dimensions of the latent inputs and, per input, whether it is a + /// float (true) or an integer index (false). std::tuple, std::vector> latent_dims() const; /// Global indices of this integrand's channels. const std::vector& channel_indices() const { return _channel_indices; } @@ -255,27 +263,24 @@ class Integrand : public FunctionGenerator { FunctionBuilder& fb, const NamedVector& args ) const override; NamedVector compute_channel_part_ret_types() const; - // Whether build_channel_part multiplies anything into weight_after_cuts: - // flavor or mirror sampling, or the merging weights and the scale veto of - // the history over every diagram. The common part leaves weight_after_cuts - // out of its product otherwise, as it is then one by construction. + // Whether build_channel_part multiplies anything into weight_after_cuts + // (flavor or mirror sampling, merging weights, scale veto). Otherwise it + // is one and the common part leaves it out. bool has_channel_weight_after_cuts() const; NamedVector build_channel_part(FunctionBuilder& fb, const NamedVector& args) const; NamedVector build_common_part(FunctionBuilder& fb, const NamedVector& channel_out) const; - // The beam densities at the scales an MLM or dynamical scale choice - // produced, for the sampled flavour. + // The beam densities at the given scales, for the sampled flavour. std::array evaluate_pdfs( FunctionBuilder& fb, const NamedVector& scales, const std::array& x, Value flavor_id ) const; - // The factors an MLM clustering puts on the event weight: the merging cut, - // the alpha_s and the pdf reweighting, each with its veto. With `ladder`, - // also the pdf reweighting ladder itself, as the systematics need it to - // vary it (see emits_pdf_ladder in integrand.cpp). + // The factors an MLM clustering puts on the event weight: merging cut, + // alpha_s and pdf reweighting, each with its veto. With `ladder`, also the + // pdf reweighting ladder (see emits_pdf_ladder in integrand.cpp). ValueVec mlm_weights( FunctionBuilder& fb, const NamedVector& scales, @@ -295,30 +300,25 @@ class Integrand : public FunctionGenerator { // for the events whose orientation is mirrored std::array, 2> _pdfs_swapped; std::array, 2> _pdf_indices; - // The pdf reweighting asks for densities the flavour sampling never does: - // the flavour of a beam line partway up the clustering ladder. One density - // over every flavour that can turn up there, picked per event by index. + // Density over every flavour a beam line can have on the MLM clustering + // ladder, picked per event by index. std::optional _pdf_rw; - // Flavour class (as the mlm kernel emits it) and sampled flavour option to - // an index into that density, flattened as class * option_count + option, - // with the class offsets alongside so that the two can be added as ints. + // Index into _pdf_rw for (flavour class from the mlm kernel, flavour option), + // flattened as class * option_count + option; offsets hold class * option_count. std::vector _pdf_rw_table; std::vector _pdf_rw_class_offsets; // pdg id of each parton of _pdf_rw, as a float for gather std::vector _pdf_rw_pdgs; std::optional _running_coupling; std::optional _energy_scale; - // One row with a 1 for every matrix-element diagram the MLM clustering has - // a history of its own for, used to restrict the per-event diagram pick. + // One row with a 1 for every diagram that has its own MLM clustering history. nested_vector2 _mlm_diagram_mask; - // MLMClustering::diagram_start_states padded to the matrix element's - // diagram count, so that any diagram the pick can return has an entry. + // MLMClustering::diagram_start_states padded to the diagram count. std::vector _mlm_start_states; // see mlm_history_qcd_power in the constructor; -1 re-evaluates int _mlm_history_qcd_power = -1; - // abs pdg id of every external leg per flavour option, row-major, for the - // clusterings that only exist for some flavours of a merged subprocess. - // Empty when the clustering does not depend on the flavours. + // abs pdg id of every external leg per flavour option, row-major. Empty when + // the clustering does not depend on the flavours. std::vector _mlm_leg_flavors; std::optional _prop_chan_weights; std::optional _subchan_weights; @@ -388,14 +388,14 @@ class IntegrandChannelPart : public FunctionGenerator { /** * The channel-independent second half of an @ref Integrand. * - * Runs the matrix element, cuts and weight assembly on the batch merged across - * channels by @ref IntegrandChannelPart. + * Runs the matrix element and the weight assembly on the accepted events of + * the batch merged across channels (see @ref IntegrandConcatenator). * * **Arguments** * - the intermediate quantities produced by @ref IntegrandChannelPart. * * **Returns** - * - the finished per-event quantities, re-split by @ref IntegrandConcatenator. + * - the same quantities as @ref Integrand. * * **References** * - [1] T. Heimel, O. Mattelaer, R. Winterhalder, "MadSpace", @@ -415,16 +415,17 @@ class IntegrandCommonPart : public FunctionGenerator { }; /** - * Re-splits the @ref IntegrandCommonPart output back to per-channel batches. + * Concatenates two @ref IntegrandChannelPart outputs along the batch. * - * The inverse of the merge done by @ref IntegrandChannelPart, so the final - * per-channel weights and events can be written out. + * The `indices_acc` of the second one are offset by the batch size of the + * first, so the result can be fed to @ref IntegrandCommonPart. * * **Arguments** - * - the merged output of @ref IntegrandCommonPart. + * - two sets of channel-part outputs, with the keys prefixed `arg1_` and + * `arg2_`. * * **Returns** - * - the same quantities, grouped back by channel. + * - one set of channel-part outputs. * * **References** * - [1] T. Heimel, O. Mattelaer, R. Winterhalder, "MadSpace", @@ -446,16 +447,15 @@ class IntegrandConcatenator : public FunctionGenerator { /** * Evaluates several @ref Integrand channels on one merged batch. * - * Partitions the batch by the per-channel counts, runs each @ref Integrand on - * its slice and concatenates the results, the same pattern as - * @ref MultiChannelMapping. + * Runs the channel part of each @ref Integrand on its slice of the batch, + * concatenates the results and runs the common part of the first integrand once + * on the merged batch. * * **Arguments** - * - the union of the channel integrands' arguments, plus the per-channel batch - * sizes. + * - `batch_sizes` – the number of events per channel. * * **Returns** - * - the concatenated per-event quantities, plus the per-channel sizes when + * - the per-event quantities of @ref Integrand, plus `return_batch_sizes` when * @p return_sizes is set. * * **References** @@ -484,15 +484,15 @@ class MultiChannelIntegrand : public FunctionGenerator { * The sampling density of an @ref Integrand at a given phase-space point. * * Evaluates the probability with which the integrand's adaptive samplers and - * channel selection would have produced a given point. Used to compute - * importance weights when replaying stored events. + * permutation / flavour selection would have produced a given point. Used for + * the density of stored points in MadNIS training. * * **Arguments** - * - the latent coordinates and discrete indices of a stored point; see - * @ref Integrand. + * - `latent`, `channel_index_in_group`, and with an adaptive flavour sampler + * `discrete_flavor_index` (plus `pdf_prior` if the integrand has one). * * **Returns** - * - `probability` – `float`, shape `(batch,)` – the sampling density. + * - `prob` – `float`, shape `(batch,)` – the sampling density. * * **References** * - [1] T. Heimel, O. Mattelaer, R. Winterhalder, "MadSpace", diff --git a/madspace/include/madspace/phasespace/invariants.hpp b/madspace/include/madspace/phasespace/invariants.hpp index 53d72cf945..abaa454a82 100644 --- a/madspace/include/madspace/phasespace/invariants.hpp +++ b/madspace/include/madspace/phasespace/invariants.hpp @@ -35,11 +35,10 @@ namespace madspace { * mean of its two edge values. This is the mapping of a `$`-excluded * propagator, whose matrix element vanishes in that window. * - * When @p mass is zero and `invariant_min` is zero, a small - * auxiliary negative @f$m^2 = -a@f$ is used inside the mapping only. This keeps - * the power-law and logarithmic forms well defined at the boundary and leaves - * the physical integrand unchanged. The naive choice @f$\nu = 2@f$ is rarely - * optimal, so @p power defaults to `0.8`. + * In the power-law and logarithmic forms the pole is shifted to + * @f$m^2 - 0.01@f$ (in the units of the invariant), which keeps them finite + * at a boundary on the pole. The naive choice @f$\nu = 2@f$ is rarely + * optimal; @ref PhaseSpaceMapping uses @f$\nu = 0.8@f$ by default. * * `batch` is the leading batch dimension. * @@ -53,7 +52,7 @@ namespace madspace { * **Outputs** * - `invariant` – `float`, shape `(batch,)` – the sampled invariant. * - * In addition every mapping returns a `weight` (`float`, shape `(batch,)`), the + * In addition every mapping returns a `det` (`float`, shape `(batch,)`), the * Jacobian of the transformation. * * **References** @@ -66,8 +65,7 @@ class Invariant : public Mapping { * @param power Exponent @f$\nu@f$ of the @f$1/(s - m^2)^\nu@f$ power-law * sampling: `0` is flat, `1` the logarithmic limit, any other * value uses @f$G_\nu@f$. Ignored when @p width is nonzero. - * @param mass Pole / resonance mass @f$m@f$; `0` triggers the boundary - * regularization described above. + * @param mass Pole / resonance mass @f$m@f$. * @param width Resonance width @f$\Gamma@f$; a nonzero value selects * Breit-Wigner sampling around @p mass. * @param flat_window Half-width @f$w@f$, in units of @p width, of the diff --git a/madspace/include/madspace/phasespace/luminosity.hpp b/madspace/include/madspace/phasespace/luminosity.hpp index 7c54cf8351..f4b495f3e1 100644 --- a/madspace/include/madspace/phasespace/luminosity.hpp +++ b/madspace/include/madspace/phasespace/luminosity.hpp @@ -41,7 +41,7 @@ namespace madspace { * - `s_hat` – `float`, shape `(batch,)` – partonic invariant * @f$\hat s = x_1 x_2 s_\mathrm{lab}@f$. * - * In addition every mapping returns a `weight` (`float`, shape `(batch,)`), the + * In addition every mapping returns a `det` (`float`, shape `(batch,)`), the * Jacobian of the transformation. * * **References** diff --git a/madspace/include/madspace/phasespace/madnis.hpp b/madspace/include/madspace/phasespace/madnis.hpp index 39694168ec..6ae04b5cb4 100644 --- a/madspace/include/madspace/phasespace/madnis.hpp +++ b/madspace/include/madspace/phasespace/madnis.hpp @@ -12,17 +12,18 @@ namespace madspace { * * Combines the per-channel integrand and sampling-probability values into the * variance-based loss used to train the @ref Flow and @ref ChannelWeightNetwork - * [1]. A dedicated study of `madspace` with MadNIS is left to a future - * publication, so this class is described from the code only. + * [1]. * * `batch` is the leading batch dimension. `c` indexes the channels. * * **Arguments** * - `chan_integrand` – `float`, shape `(batch,)` – integrand values. * - `chan_sample_prob` – `float`, shape `(batch,)` – sampling probabilities. - * - `chan_cwnet_inputs`, `chan_channel_weight_values` / - * `chan_channel_weight_indices` – present only with a - * @ref ChannelWeightNetwork. + * - With a @ref ChannelWeightNetwork also `chan_cwnet_inputs`, + * `chan_channel_weights` (or the compressed pair + * `chan_channel_weight_values` / `chan_channel_weight_indices`) and + * `chan_channel_indices`. + * - `chan_` – the arguments of the c-th function of @p functions. * * **Returns** * - `loss` – `float`, scalar – the training loss. @@ -36,7 +37,9 @@ namespace madspace { class MadnisLoss : public FunctionGenerator { public: /** - * @param functions The per-channel integrands. + * @param functions The per-channel integrands; the + * first return value of each is the + * flow probability. * @param cwnet Optional channel-weight network to * train jointly. * @param softclip_threshold Soft-clipping threshold on the diff --git a/madspace/include/madspace/phasespace/matrix_element.hpp b/madspace/include/madspace/phasespace/matrix_element.hpp index dfccc64cd6..3e24210a73 100644 --- a/madspace/include/madspace/phasespace/matrix_element.hpp +++ b/madspace/include/madspace/phasespace/matrix_element.hpp @@ -40,7 +40,7 @@ class MatrixElement : public FunctionGenerator { public: /// A quantity the matrix element can be asked to consume. enum MatrixElementInput { - momenta_in, ///< Final-state momenta. + momenta_in, ///< Momenta of all external particles. alpha_s_in, ///< Strong coupling. flavor_in, ///< Flavour assignment. random_color_in, ///< Random number for the color draw. @@ -85,18 +85,12 @@ class MatrixElement : public FunctionGenerator { * @param me_frame One-based indices of the external particles * whose momentum sum defines the rest frame * the matrix element is evaluated in (the run - * card's me_frame). Empty means no boost, so - * the matrix element sees the momenta in the - * frame they are generated in. + * card's me_frame). Empty means no boost. * @param incoming_count How many of the external particles are - * incoming. The frame is reached the way - * madevent reaches it, starting from the rest - * frame of the incoming system (the partonic - * centre of mass of a collision, the decaying - * particle's rest frame of a decay): boosts do - * not commute, so going there straight from - * the lab frame would leave a Wigner rotation - * behind and rotate the polarisation axes. + * incoming. The me_frame boost starts from the + * rest frame of the incoming system, as in + * madevent, to avoid a Wigner rotation of the + * polarisation axes. */ MatrixElement( std::size_t matrix_element_index, diff --git a/madspace/include/madspace/phasespace/mlm_clustering.hpp b/madspace/include/madspace/phasespace/mlm_clustering.hpp index 58fa8c9dc3..439b6106d3 100644 --- a/madspace/include/madspace/phasespace/mlm_clustering.hpp +++ b/madspace/include/madspace/phasespace/mlm_clustering.hpp @@ -110,8 +110,7 @@ class MLMClustering : public FunctionGenerator { // MLM veto can never trip on it. double cm_energy, JetScaleScheme jet_scale_scheme = JetScaleScheme::production, - // Left at the existing definition by default: unlike - // jet_scale_scheme, this one moves the cross section. + // Unlike jet_scale_scheme, this choice changes the cross section. ScaleScheme scale_scheme = ScaleScheme::clustering_mean, // Signed color representation per pdg id, as exported in the // subprocess metadata. Used to follow a parton line through the @@ -119,8 +118,8 @@ class MLMClustering : public FunctionGenerator { // that is not listed. std::unordered_map pdg_color_types = {}, // Generation-level merging cut, the counterpart of madevent's xqcut. - // A clustering that emitted a jet below this scale drops the event. - // Zero disables it. + // A clustering that emitted a jet below this scale sets xqcut_weight + // to 0. Zero disables it. double xqcut = 0, double bw_cutoff = 15, // D of the final-state kt measure; 1 as madevent's run_card d and FxFx. @@ -247,8 +246,8 @@ class MLMClustering : public FunctionGenerator { }; // The clustering along one diagram as a function of its own, taking the -// diagram index as a second argument. The integrand calls build_along_diagram -// directly; this is how that path is reached from Python. +// diagram index as a second argument. The integrand calls +// build_from_start_state directly; this is how that path is reached from Python. class MLMClusteringAlongDiagram : public FunctionGenerator { public: MLMClusteringAlongDiagram(const MLMClustering& clustering); diff --git a/madspace/include/madspace/phasespace/mlp.hpp b/madspace/include/madspace/phasespace/mlp.hpp index 75d34f0bcb..c10f102976 100644 --- a/madspace/include/madspace/phasespace/mlp.hpp +++ b/madspace/include/madspace/phasespace/mlp.hpp @@ -11,11 +11,12 @@ namespace madspace { /** * Fully connected neural network as a compute-graph function. * - * A stack of @p layers dense layers of width @p hidden_dim with @p activation - * between them and a linear output layer. Used as the subnetwork of the - * @ref Flow and @ref DiscreteFlow adaptive samplers [1]. The weights and biases - * are compute-graph globals named with @p prefix. Call - * `initialize_globals(context)` once before use. + * A stack of @p layers dense layers: the first `layers - 1` have width + * @p hidden_dim and @p activation, the last one is linear. Used as the + * subnetwork of the @ref Flow, @ref DiscreteFlow and @ref ChannelWeightNetwork + * [1]. The weights and biases are compute-graph globals named with @p prefix. + * Call `initialize_globals(context)` once before use; the output layer starts + * at zero. * * `batch` is the leading batch dimension. * @@ -37,7 +38,7 @@ class MLP : public FunctionGenerator { * @param input_dim Input width. * @param output_dim Output width. * @param hidden_dim Hidden-layer width. - * @param layers Number of layers. + * @param layers Number of dense layers, including the output layer. * @param activation Activation between the hidden layers. * @param prefix Prefix for the trainable global names. */ diff --git a/madspace/include/madspace/phasespace/multichannel.hpp b/madspace/include/madspace/phasespace/multichannel.hpp index 763ad074c9..92af56a59e 100644 --- a/madspace/include/madspace/phasespace/multichannel.hpp +++ b/madspace/include/madspace/phasespace/multichannel.hpp @@ -31,7 +31,7 @@ namespace madspace { * **Outputs** * - the outputs of the channel mappings (identical across channels). * - * In addition every mapping returns a `weight` (`float`, shape `(batch,)`), the + * In addition every mapping returns a `det` (`float`, shape `(batch,)`), the * Jacobian of the transformation. * * **References** @@ -80,7 +80,8 @@ class MultiChannelMapping : public Mapping { * is partitioned by the per-channel counts in the `batch_sizes` argument. Each * slice is passed through its channel function, and the results are * concatenated. All channel functions must share the same argument and return - * layout, which this class then exposes (Sec. 2.1.1 of [1]). + * layout, which this class then exposes (Sec. 2.1.1 of [1]). A batch-size + * argument of the channel functions is replaced by the per-channel size. * * `batch` is the leading batch dimension. * @@ -90,7 +91,8 @@ class MultiChannelMapping : public Mapping { * * **Returns** * - the returns of the channel functions, plus - * - `batch_sizes` – echoed back, only when @p return_batch_sizes is true. + * - `batch_sizes` – the per-channel sizes of the concatenated first return + * value, only when @p return_batch_sizes is true. * * **References** * - [1] T. Heimel, O. Mattelaer, R. Winterhalder, "MadSpace", @@ -100,8 +102,8 @@ class MultiChannelFunction : public FunctionGenerator { public: /** * @param functions The per-channel functions. - * @param return_batch_sizes If true, the `batch_sizes` argument is also - * returned. + * @param return_batch_sizes If true, the per-channel batch sizes of the + * result are also returned. */ MultiChannelFunction( const std::vector>& functions, diff --git a/madspace/include/madspace/phasespace/observable.hpp b/madspace/include/madspace/phasespace/observable.hpp index d8e5fd95b8..bbd6188da9 100644 --- a/madspace/include/madspace/phasespace/observable.hpp +++ b/madspace/include/madspace/phasespace/observable.hpp @@ -9,30 +9,33 @@ namespace madspace { /** - * Kinematic observable evaluated for a selection of outgoing particles. + * Kinematic observable evaluated for a selection of particles. * - * Selects particles by PDG id (@p pids), optionally groups them into tuples - * (@p select_pids) and sums the momenta of each tuple, then evaluates one - * @ref ObservableOption for every selection [1]. Several PDG types may be - * combined, for example all jet flavours. The available observables are - * single-momentum functions (`e`, `px`, `py`, `pz`, `mass`, `pt`, `p_mag`, + * @p pids lists the PDG ids of all particles of the process, in the order of + * the momenta. Each entry of @p select_pids is a set of PDG ids that selects + * matching particles; selections are combined into tuples whose momenta can be + * summed, and one @ref ObservableOption is evaluated per tuple [1]. Several PDG + * types may be combined, for example all jet flavours. The available + * observables are single-momentum functions (`e`, `px`, `py`, `pz`, `mass`, `pt`, `p_mag`, * `phi`, `theta`, `y`, `y_abs`, `eta`, `eta_abs`), pair functions * (`delta_eta`, `delta_phi`, `delta_r`, `pair_mass`, `sfos_pair_mass`), and * the event-level `sqrt_s`. `sfos_pair_mass` is `pair_mass` restricted to * same-flavour opposite-sign pairs, i.e. pairs whose PDG ids are `id` and * `-id` (the l+ l- pairs of MadEvent's `mmll`); it needs the signed PDG ids - * of the actual flavours in @p pids and cannot be combined with ordering. The same mechanism defines the observables used by - * @ref Cuts and @ref ObservableHistograms. + * of the actual flavours in @p pids and cannot be combined with ordering. The + * same mechanism defines the observables used by @ref Cuts and + * @ref ObservableHistograms. * - * `batch` is the leading batch dimension. `n` is the number of selected - * particles and `k` the number of selections. + * `batch` is the leading batch dimension. `n` is `len(pids)` and `k` the + * number of tuples. * * **Arguments** - * - `momenta` – `float`, shape `(batch, n, 4)` – the selected four-momenta. + * - `momenta` – `float`, shape `(batch, n, 4)` – the momenta of all particles. * * **Returns** * - `observable` – `float`, shape `(batch,)` or `(batch, k)` – the observable - * value, one per selection unless @p sum_observable collapses them. + * value, one per tuple unless @p sum_momenta or @p sum_observable collapses + * them. It is 0 if the selection matches no particle. * * **References** * - [1] T. Heimel, O. Mattelaer, R. Winterhalder, "MadSpace", @@ -40,7 +43,7 @@ namespace madspace { */ class Observable : public FunctionGenerator { public: - /// PDG ids commonly grouped as a jet. + /// PDG ids of the light quarks and the gluon, grouped as a jet. static const std::vector jet_pids; /// PDG ids of the b quark. static const std::vector bottom_pids; @@ -75,18 +78,24 @@ class Observable : public FunctionGenerator { }; /** - * @param pids PDG ids selecting the outgoing particles. + * @param pids PDG ids of all particles of the process, in the + * order of the momenta. * @param observable Which @ref ObservableOption to compute. - * @param select_pids Groups of PDG ids forming the tuples the pair / - * summed observables act on; empty selects every - * matching particle individually. + * @param select_pids One set of PDG ids per selection. Single-momentum + * observables need at least one selection, pair + * observables one or two (one is paired with + * itself) and `sqrt_s` none. * @param sum_momenta Sum the four-momenta within each tuple before * evaluating the observable. * @param sum_observable Sum the computed observable over all selections. - * @param order_observable Optional observable used to sort the selections - * before @p order_indices is applied. - * @param order_indices After sorting by @p order_observable, keep only - * these positions. + * @param order_observable Optional observable used to sort the particles + * of each selection before @p order_indices is + * applied. + * @param order_indices Per selection, the position to keep after + * sorting by @p order_observable: positive values + * count from the end of the sorted list (1 is the + * last), negative ones from the start (-1 is the + * first), 0 applies no ordering. * @param ignore_incoming Exclude the two incoming partons from the * selection. * @param name Label for the returned value. @@ -123,7 +132,7 @@ class Observable : public FunctionGenerator { } /// The label passed to the constructor. std::string name() const { return _name; } - /// True when no particle in the event matches the requested @p pids. + /// True when a selection matches no particle, so the observable is 0. bool not_found() const; private: diff --git a/madspace/include/madspace/phasespace/pdf.hpp b/madspace/include/madspace/phasespace/pdf.hpp index 599b9e87c1..612e64d768 100644 --- a/madspace/include/madspace/phasespace/pdf.hpp +++ b/madspace/include/madspace/phasespace/pdf.hpp @@ -25,17 +25,19 @@ struct PdfGrid { std::vector logq2; /// PDG ids of the flavours in the grid. std::vector pids; - /// Grid values, indexed by flavour then by `(x, q)` knot. + /// Grid values, indexed by `(q, x)` grid point (`q_index * x.size() + x_index`), + /// then by flavour. std::vector> values; - /// Knot counts of the sub-grid regions. + /// Number of `q` intervals in each sub-grid region. std::vector region_sizes; /// Load an LHAPDF-format grid. /// @param file Path to the grid file. PdfGrid(const std::string& file); - /// Total number of `(x, q)` grid points. + /// Number of `(x, q)` grid points, including padding, as stored in the + /// coefficient tensor. std::size_t grid_point_count() const; - /// Number of scale knots. + /// Number of scale knots, counting the knots shared by two regions once. std::size_t q_count() const; /// Fill @p tensor with the interpolation coefficients. void initialize_coefficients(Tensor tensor) const; @@ -59,15 +61,15 @@ struct PdfGrid { * Interpolates a @ref PdfGrid at the sampled @f$(x, q)@f$, returning * @f$x f(x, q)@f$ for the requested flavours; the numerical output agrees with * LHAPDF to double precision [1] (Sec. 3.2.3 of [2]). With a @ref CustomPdf, - * the user-supplied library is called once per requested flavour instead. + * the user-supplied library is called for each requested flavour instead. * * `batch` is the leading batch dimension. * * **Arguments** * - `x` – `float`, shape `(batch,)` – momentum fraction. * - `q` – `float`, shape `(batch,)` – factorization scale. - * - `flavor_index` – `int`, shape `(batch,)` – which flavour to return. - * Present only when @p dynamic_pid is true. + * - `flavor_index` – `int`, shape `(batch,)` – index into @p pids selecting + * the flavour to return. Present only when @p dynamic_pid is true. * * **Returns** * - `pdf` – `float`, shape `(batch,)` when @p dynamic_pid is true, otherwise @@ -84,8 +86,8 @@ class PartonDensity : public FunctionGenerator { /** * @param grid The parton-density grid to interpolate. * @param pids PDG ids of the flavours to return. - * @param dynamic_pid If true, a single flavour selected per event by the - * `flavor_index` argument is returned. + * @param dynamic_pid If true, the single flavour of @p pids selected per + * event by `flavor_index` is returned. * @param prefix Prefix for the grid global names. */ PartonDensity( @@ -98,8 +100,8 @@ class PartonDensity : public FunctionGenerator { * @param custom_pdf The user-supplied parton density. * @param pids PDG ids of the flavours to return. * @param beam_index Index of the beam (0 or 1), passed to the library. - * @param dynamic_pid If true, a single flavour selected per event by the - * `flavor_index` argument is returned. + * @param dynamic_pid If true, the single flavour of @p pids selected per + * event by `flavor_index` is returned. */ PartonDensity( const CustomPdf& custom_pdf, @@ -137,13 +139,13 @@ struct AlphaSGrid { std::vector logq2; /// `alpha_s` values at the knots. std::vector values; - /// Knot counts of the sub-grid regions. + /// Number of `q` intervals in each sub-grid region. std::vector region_sizes; /// Load an LHAPDF-format `alpha_s` grid. /// @param file Path to the grid file. AlphaSGrid(const std::string& file); - /// Number of scale knots. + /// Number of scale knots, counting the knots shared by two regions once. std::size_t q_count() const; /// Fill @p tensor with the interpolation coefficients. void initialize_coefficients(Tensor tensor) const; diff --git a/madspace/include/madspace/phasespace/phasespace.hpp b/madspace/include/madspace/phasespace/phasespace.hpp index b1b5693fef..a316355c36 100644 --- a/madspace/include/madspace/phasespace/phasespace.hpp +++ b/madspace/include/madspace/phasespace/phasespace.hpp @@ -14,7 +14,8 @@ namespace madspace { /** - * Full @f$2 \to n@f$ phase-space mapping for one diagram topology. + * Full @f$2 \to n@f$ (or @f$1 \to n@f$ decay) phase-space mapping for one + * diagram topology. * * Assembles a complete mapping from the elementary building blocks along the * recursive decomposition of the tree-level phase space (Sec. 2.2 of [1]), @@ -52,12 +53,12 @@ namespace madspace { * `permutation_index`. * * **Outputs** - * - `momenta` – `float`, shape `(batch, n_out + 2, 4)` – all momenta (incoming - * beams first, then outgoing). + * - `momenta` – `float`, shape `(batch, particle_count(), 4)` – all momenta + * (incoming first, then outgoing), in the lab frame. * - `x1` – `float`, shape `(batch,)` – first parton momentum fraction. * - `x2` – `float`, shape `(batch,)` – second parton momentum fraction. * - * In addition every mapping returns a `weight` (`float`, shape `(batch,)`), the + * In addition every mapping returns a `det` (`float`, shape `(batch,)`), the * Jacobian of the transformation. * * **References** @@ -226,8 +227,7 @@ class PhaseSpaceMapping : public Mapping { const Cuts& cuts() const { return _cuts; } /// True when the cuts exclude the whole region this mapping samples: a /// pair mass cut puts a propagator's floor at or above the upper end of - /// its on-shell window. Every point then fails the cuts, so the channel - /// contributes nothing and should be dropped rather than sampled. + /// its on-shell window. The channel contributes nothing and can be dropped. bool empty() const { return _empty; } private: diff --git a/madspace/include/madspace/phasespace/rambo.hpp b/madspace/include/madspace/phasespace/rambo.hpp index adbaf423d2..3bea3e4cc7 100644 --- a/madspace/include/madspace/phasespace/rambo.hpp +++ b/madspace/include/madspace/phasespace/rambo.hpp @@ -51,7 +51,7 @@ namespace madspace { * - `momentum_i` – `float`, shape `(batch, 4)` – the `n_particles` final-state * four-momenta. * - * In addition every mapping returns a `weight` (`float`, shape `(batch,)`), the + * In addition every mapping returns a `det` (`float`, shape `(batch,)`), the * Jacobian of the transformation. * * **References** diff --git a/madspace/include/madspace/phasespace/scale.hpp b/madspace/include/madspace/phasespace/scale.hpp index c615c8b4bd..d8e179ee9e 100644 --- a/madspace/include/madspace/phasespace/scale.hpp +++ b/madspace/include/madspace/phasespace/scale.hpp @@ -14,20 +14,19 @@ namespace madspace { * (Sec. 3.2.6 of [1], matching the choices in [2]): * * @f[ - * \mu = \sum_i m_i, \qquad * \mu = E_\mathrm{T} = \sum_i \frac{E_i\,p_{\mathrm{T},i}}{|\vec p_i|}, - * @f] - * @f[ + * \qquad * \mu = H_\mathrm{T} = \sum_i \sqrt{m_i^2 + p_{\mathrm{T},i}^2}, * @f] * - * with @ref DynamicalScaleType selecting @f$E_\mathrm{T}@f$ - * (`transverse_energy`), @f$H_\mathrm{T}@f$ (`transverse_mass`), + * with sums over the outgoing particles, and @ref DynamicalScaleType selecting + * @f$E_\mathrm{T}@f$ (`transverse_energy`), @f$H_\mathrm{T}@f$ (`transverse_mass`), * @f$H_\mathrm{T}/2@f$ (`half_transverse_mass`) or @f$\sqrt{\hat s}@f$ * (`partonic_energy`). The dynamical scale is multiplied by `scale_factor`, * e.g. @f$H_\mathrm{T}/4@f$ is `half_transverse_mass` with `scale_factor = 0.5`. * A @ref CustomFunction of type CustomFunction::scale replaces the dynamical - * choice, like `user_dynamical_scale` in MadGraph5. + * choice (also multiplied by `scale_factor`), like `user_dynamical_scale` in + * MadGraph5. * * `batch` is the leading batch dimension. * @@ -117,8 +116,8 @@ class EnergyScale : public FunctionGenerator { * @param ren_scale Fixed renormalization-scale value. * @param fact_scale1 Fixed factorization scale for the first beam. * @param fact_scale2 Fixed factorization scale for the second beam. - * @param scale_factor Factor on the dynamical scale; fixed scales are - * left alone. + * @param scale_factor Factor on the dynamical or custom scale; fixed + * scales are left alone. * @param custom_scale Optional user-supplied function that replaces * the dynamical scale choice. * @param min_scale Floor on mu_R and mu_F. The scales are clamped diff --git a/madspace/include/madspace/phasespace/t_propagator_mapping.hpp b/madspace/include/madspace/phasespace/t_propagator_mapping.hpp index 4709bd2007..c142fb7581 100644 --- a/madspace/include/madspace/phasespace/t_propagator_mapping.hpp +++ b/madspace/include/madspace/phasespace/t_propagator_mapping.hpp @@ -39,7 +39,7 @@ namespace madspace { * - `momentum_i` – `float`, shape `(batch, 4)` – the `kappa + 3` momenta * (incoming and outgoing) of the `t`-channel chain. * - * In addition every mapping returns a `weight` (`float`, shape `(batch,)`), the + * In addition every mapping returns a `det` (`float`, shape `(batch,)`), the * Jacobian of the transformation. * * **References** diff --git a/madspace/include/madspace/phasespace/three_particle.hpp b/madspace/include/madspace/phasespace/three_particle.hpp index 2ba277ccd3..355d689ab7 100644 --- a/madspace/include/madspace/phasespace/three_particle.hpp +++ b/madspace/include/madspace/phasespace/three_particle.hpp @@ -40,7 +40,7 @@ namespace madspace { * - `momentum1`, `momentum2`, `momentum3` – `float`, shape `(batch, 4)` – the * daughter momenta. * - * In addition every mapping returns a `weight` (`float`, shape `(batch,)`), the + * In addition every mapping returns a `det` (`float`, shape `(batch,)`), the * Jacobian of the transformation. * * **References** @@ -135,7 +135,7 @@ class ThreeBodyDecay : public Mapping { * - `momentum1`, `momentum2` – `float`, shape `(batch, 4)` – the two resolved * momenta. * - * In addition every mapping returns a `weight` (`float`, shape `(batch,)`), the + * In addition every mapping returns a `det` (`float`, shape `(batch,)`), the * Jacobian of the transformation. * * **References** diff --git a/madspace/include/madspace/phasespace/topology.hpp b/madspace/include/madspace/phasespace/topology.hpp index 96f48b1240..f0bc85036c 100644 --- a/madspace/include/madspace/phasespace/topology.hpp +++ b/madspace/include/madspace/phasespace/topology.hpp @@ -220,7 +220,7 @@ class Topology { const std::vector& decay_integration_order() const { return _decay_integration_order; } - /// Diagram indices of the outgoing particles, in decay-tree order. + /// Decay-tree node index of each outgoing particle, in diagram order. const std::vector& outgoing_indices() const { return _outgoing_indices; } @@ -232,8 +232,9 @@ class Topology { /// A decay has no `t`-channel chain and a root virtuality fixed by the /// mass of the decaying particle. bool is_decay() const { return _incoming_masses.size() == 1; } - /// For each propagator, the outgoing momenta summed to form it and the - /// energy window `(indices, e_min, e_max)`. + /// For each propagator, a tuple `(factors, mass, width)`. `factors` has one + /// entry per external particle (incoming first): +1 or -1 for the momenta + /// summed to form the propagator momentum, 0 otherwise. /// @param only_decays Restrict the result to the `s`-channel decay nodes. std::vector, double, double>> propagator_momentum_terms(bool only_decays = false) const; diff --git a/madspace/include/madspace/phasespace/two_particle.hpp b/madspace/include/madspace/phasespace/two_particle.hpp index 2cf3ea47b2..5bedb4c8bb 100644 --- a/madspace/include/madspace/phasespace/two_particle.hpp +++ b/madspace/include/madspace/phasespace/two_particle.hpp @@ -17,11 +17,11 @@ namespace madspace { * sampled uniformly, * * @f[ - * \phi = 2\pi\,r_\phi, \qquad \cos\theta = 2\,r_{\cos\theta} - 1. + * \phi = \pi\,(2 r_\phi - 1), \qquad \cos\theta = 2\,r_{\cos\theta} - 1. * @f] * * The momenta are then boosted back (Sec. 2.2.3 of [1]). The measure is flat - * in @f$(\phi, \cos\theta)@f$. + * in @f$(\phi, \cos\theta)@f$ unless a bound narrows the @f$\cos\theta@f$ range. * * `batch` is the leading batch dimension. The masses are inputs here. They are * supplied on the forward call and recovered by the inverse. @@ -44,7 +44,7 @@ namespace madspace { * - `momentum1` – `float`, shape `(batch, 4)` – first daughter momentum. * - `momentum2` – `float`, shape `(batch, 4)` – second daughter momentum. * - * In addition every mapping returns a `weight` (`float`, shape `(batch,)`), the + * In addition every mapping returns a `det` (`float`, shape `(batch,)`), the * Jacobian of the transformation. * * **References** @@ -101,7 +101,7 @@ class TwoBodyDecay : public Mapping { * Same two-particle phase space as @ref TwoBodyDecay. The polar angle is traded * for the Mandelstam invariant @f$t = (p_\mathrm{in,1} - p_1)^2 < 0@f$, which * is linear in @f$\cos\theta@f$ [1]. The azimuth is uniform, - * @f$\phi = 2\pi\,r_\phi@f$. @f$|t|@f$ is drawn with an @ref Invariant (from + * @f$\phi = \pi\,(2 r_\phi - 1)@f$. @f$|t|@f$ is drawn with an @ref Invariant (from * @p invariant_power / @p mass / @p width) so a t-channel propagator pole is * flattened (Sec. 2.2.4 of [1]). The momenta are built in the scattering * center-of-mass frame and boosted out. @@ -129,7 +129,7 @@ class TwoBodyDecay : public Mapping { * - `momentum1` – `float`, shape `(batch, 4)` – first outgoing momentum. * - `momentum2` – `float`, shape `(batch, 4)` – second outgoing momentum. * - * In addition every mapping returns a `weight` (`float`, shape `(batch,)`), the + * In addition every mapping returns a `det` (`float`, shape `(batch,)`), the * Jacobian of the transformation. * * **References** @@ -237,7 +237,7 @@ class TwoToTwoParticleScattering : public Mapping { * - `momentum1` – `float`, shape `(batch, 4)` – first outgoing momentum. * - `momentum2` – `float`, shape `(batch, 4)` – recoil momentum. * - * In addition every mapping returns a `weight` (`float`, shape `(batch,)`), the + * In addition every mapping returns a `det` (`float`, shape `(batch,)`), the * Jacobian of the transformation. * * **References** diff --git a/madspace/include/madspace/phasespace/unweighter.hpp b/madspace/include/madspace/phasespace/unweighter.hpp index 23d8202f8a..14c53a0409 100644 --- a/madspace/include/madspace/phasespace/unweighter.hpp +++ b/madspace/include/madspace/phasespace/unweighter.hpp @@ -42,12 +42,10 @@ class Unweighter : public FunctionGenerator { * The maximum weight is the @p quantile quantile of |w| over the batch (so a * fraction 1 - @p quantile of the events is over-weight). An event is kept * with probability min(1, |w| / w_max) and gets the weight - * sign(w) * max(|w|, w_max): the sign of a negative weight (an interference) - * is kept, and the quantile is over |w| like the acceptance, so that a mostly - * negative batch is still unweighted. The `adaptive_prob` of a kept event, the - * density it was sampled with, is multiplied by its acceptance probability and - * by N / N_kept, so that training on the buffered events stays unbiased - * (Sec. 3.4.1 of [1]). + * sign(w) * max(|w|, w_max), so the sign of a negative weight is kept. The + * `adaptive_prob` of a kept event, the density it was sampled with, is + * multiplied by its acceptance probability and by N / N_kept, so that training + * on the buffered events stays unbiased (Sec. 3.4.1 of [1]). * * `batch` is the leading batch dimension. * diff --git a/madspace/include/madspace/phasespace/vegas.hpp b/madspace/include/madspace/phasespace/vegas.hpp index a17632fedf..fa03317eb1 100644 --- a/madspace/include/madspace/phasespace/vegas.hpp +++ b/madspace/include/madspace/phasespace/vegas.hpp @@ -61,7 +61,7 @@ class VegasHistogram : public FunctionGenerator { * - `data` – `float`, shape `(batch, dimension)` – the reparametrized * coordinates. * - * In addition every mapping returns a `weight` (`float`, shape `(batch,)`), the + * In addition every mapping returns a `det` (`float`, shape `(batch,)`), the * Jacobian of the transformation. * * **References** @@ -82,7 +82,7 @@ class VegasMapping : public Mapping { ); /// Global name of the VEGAS grid. const std::string& grid_name() const { return _grid_name; } - /// Register the grid global on @p context. + /// Define the grid global on @p context and fill it with a uniform grid. void initialize_globals(ContextPtr context) const; /// Number of dimensions. std::size_t dimension() const { return _dimension; } @@ -106,8 +106,8 @@ class VegasMapping : public Mapping { std::string _grid_name; }; -/// Register the VEGAS grid global named @p grid_name on @p context; see -/// @ref VegasMapping::initialize_globals. +/// Fill the already defined VEGAS grid global @p grid_name on @p context with a +/// uniform grid. void initialize_vegas_grid(ContextPtr context, const std::string& grid_name); } // namespace madspace diff --git a/madspace/include/madspace/util.hpp b/madspace/include/madspace/util.hpp index 1fc28573ac..30de72552f 100644 --- a/madspace/include/madspace/util.hpp +++ b/madspace/include/madspace/util.hpp @@ -257,8 +257,7 @@ class NamedVector { _index_map[key] = index + old_size; } } - /// A copy reordered to match `index_map` (same keys, same key-to-position - /// mapping as this instance's, just reassigned). + /// A copy with the same keys whose values are ordered by `index_map`. NamedVector sort_like(const std::unordered_map& index_map) const { if (index_map.size() != size()) { diff --git a/madspace/install.py b/madspace/install.py index f21688e2b5..b6e7a82b58 100644 --- a/madspace/install.py +++ b/madspace/install.py @@ -40,10 +40,9 @@ SCRIPT_DIR = Path(__file__).parent.resolve() INSTALL_DIR = SCRIPT_DIR / "install" BUILD_DIR = SCRIPT_DIR / "build" -# Deliberately outside both directories, so --clean resets the build without -# also forgetting how the user wants madspace built. _LEGACY_SETTINGS_FILE is -# where it used to live; still read so an existing installation keeps its -# choices. +# Outside both directories, so --clean resets the build without also forgetting +# how the user wants madspace built. _LEGACY_SETTINGS_FILE is an older location, +# still read so an existing installation keeps its choices. SETTINGS_FILE = SCRIPT_DIR / "install_settings.json" _LEGACY_SETTINGS_FILE = BUILD_DIR / "install_settings.json" @@ -167,8 +166,8 @@ def ask_compile_options( *keys*, if given, restricts the menu to entries whose output_key is in it (used to split the compile-options prompt into a basic and an expert - stage). *show_expert_option* appends a pseudo entry ("_expert") used to - let the user opt into seeing the expert stage. + stage). *show_expert_option* appends a pseudo entry (output key "_expert") + that lets the user opt into the expert stage. *from_saved* controls the Enter hint wording. """ @@ -345,7 +344,7 @@ def resolve_gpu_arch(args: argparse.Namespace, saved: dict, backend: str) -> str # The source build (scikit-build-core) needs cmake >= 3.15 on PATH. When it is # missing (or too old) we fall back to a cmake installed through MadGraph's # HEPTools installer (`install cmake` in MG5), which lives next to this package -# under /HEPTools. An explicit MADGRAPH_CMAKE / CMAKE env var wins. +# under /HEPTools. A cmake on PATH that is recent enough is preferred. CMAKE_MIN_VERSION = (3, 15) @@ -462,8 +461,7 @@ def set_build_parallelism(env: dict, jobs: int | None) -> dict: count comes entirely from the environment. With the Ninja generator that goes unnoticed (ninja parallelises on its own), but whenever ninja is missing scikit-build-core falls back to "Unix Makefiles", and make without - ``-j`` compiles one file at a time -- which is why a gcc/make build took - minutes while the ninja one did not. + ``-j`` compiles one file at a time. ``CMAKE_BUILD_PARALLEL_LEVEL`` is read by ``cmake --build`` itself, so it works for either generator, and being an environment variable it is also @@ -494,9 +492,9 @@ def run(cmd: list, env: dict | None = None) -> None: def install_build_deps(system: bool = False) -> dict: """Install build-system dependencies and return an updated env. - Reads the list from pyproject.toml's [build-system] requires, which now - also includes pyyaml and pybind11-stubgen for the CMake-driven code - generation (see CMakeLists.txt). + Reads the list from pyproject.toml's [build-system] requires, which includes + pyyaml and pybind11-stubgen for the CMake-driven code generation (see + CMakeLists.txt). When *system* is False (default) deps are installed into INSTALL_DIR and PYTHONPATH is set so the subsequent pip invocation picks them up. diff --git a/madspace/instruction_set.yaml b/madspace/instruction_set.yaml index f78a3c0ee1..1a4c62f5eb 100644 --- a/madspace/instruction_set.yaml +++ b/madspace/instruction_set.yaml @@ -27,7 +27,7 @@ stack_sizes: outputs: - name: sizes desc: "combined batch-size list, one entry per input" - desc: "Packs the batch sizes of several batch-size-list values into one list." + desc: "Combines several single-entry batch-size lists into one list, one entry per input." class: StackSizesInstruction custom_op: True @@ -48,13 +48,13 @@ pop: outputs: - name: rest type: [float, n-1, ...] - desc: "the remaining n-1 slices" + desc: "the first n-1 slices" - name: item type: [float, ...] - desc: "the first slice" + desc: "the last slice" custom_op: True differentiable: True - desc: "Splits off the first slice along the dimension after the batch dimension." + desc: "Splits off the last slice along the dimension after the batch dimension." batch_cat: inputs: any @@ -84,8 +84,8 @@ cat: inputs: any outputs: - name: out - desc: "concatenation along the last dimension" - desc: "Concatenates several tensors along their last dimension." + desc: "concatenation along the dimension after the batch dimension" + desc: "Concatenates several tensors along the dimension after the batch dimension." class: CatInstruction custom_op: True differentiable: True @@ -110,7 +110,7 @@ offset_indices: desc: "integer offset per output element" class: OffsetIndicesInstruction custom_op: True - desc: "Builds flat offsets into a concatenated batch from two batch-size lists." + desc: "Builds an integer tensor of total length sum(batch_sizes_out). Entries of group k hold the sum of the first k entries of batch_sizes_offset." full: inputs: any @@ -124,14 +124,14 @@ full: squeeze: inputs: - name: input - desc: "tensor with at least one trailing dimension" + desc: "tensor with shape (batch, 1, ...)" outputs: - name: output - desc: "tensor with that dimension removed" + desc: "tensor with shape (batch, ...)" class: SqueezeInstruction custom_op: True differentiable: True - desc: "Removes the first trailing dimension of a tensor." + desc: "Removes the dimension after the batch dimension by taking its first entry (normally of size 1)." unsqueeze: inputs: @@ -139,7 +139,7 @@ unsqueeze: desc: "input tensor" outputs: - name: output - desc: "tensor with a leading size-1 trailing dimension" + desc: "tensor with an added size-1 dimension after the batch dimension" class: UnsqueezeInstruction custom_op: True differentiable: True @@ -156,7 +156,7 @@ accept_norm: desc: "ratio of full to accepted batch size" class: AcceptNormInstruction custom_op: True - desc: "Normalization factor relating an accepted sub-batch to the full batch." + desc: "Ratio of the full batch size to the accepted batch size, as a float scalar." batch_split_by_index: inputs: @@ -417,7 +417,7 @@ obs_sqrt_s: - name: obs type: [float] desc: "center-of-mass energy" - desc: "Invariant mass of the sum of all external momenta. See @ref Observable." + desc: "Invariant mass of the sum of the first two external momenta (the incoming particles). See @ref Observable." obs_e: inputs: @@ -523,7 +523,7 @@ obs_theta: outputs: - name: obs type: [float, ...] - desc: "polar angle" + desc: "polar angle in [0, pi]" dims: 0 desc: "Polar angle of a four-momentum. See @ref Observable." @@ -561,7 +561,7 @@ obs_eta: type: [float, ...] desc: "pseudorapidity" dims: 0 - desc: "Pseudorapidity of a four-momentum. See @ref Observable." + desc: "Pseudorapidity of a four-momentum; 99 if the momentum is (nearly) zero. See @ref Observable." obs_eta_abs: inputs: @@ -573,7 +573,7 @@ obs_eta_abs: type: [float, ...] desc: "absolute pseudorapidity" dims: 0 - desc: "Absolute pseudorapidity of a four-momentum. See @ref Observable." + desc: "Absolute pseudorapidity of a four-momentum; 99 if the momentum is (nearly) zero. See @ref Observable." obs_delta_eta: inputs: @@ -588,7 +588,7 @@ obs_delta_eta: type: [float, ...] desc: "pseudorapidity difference" dims: 0 - desc: "Pseudorapidity difference between two four-momenta. See @ref Observable." + desc: "Signed pseudorapidity difference eta(p1) - eta(p2). See @ref Observable." obs_delta_phi: inputs: @@ -603,7 +603,7 @@ obs_delta_phi: type: [float, ...] desc: "azimuthal angle difference" dims: 0 - desc: "Azimuthal angle difference between two four-momenta. See @ref Observable." + desc: "Azimuthal angle difference phi(p1) - phi(p2), wrapped to [-pi, pi). See @ref Observable." obs_delta_r: inputs: @@ -808,7 +808,7 @@ diff_cross_section: - name: result type: [float] desc: "differential cross section" - desc: "Assembles the differential cross section from PDFs, the matrix element and the flux factor. See @ref DifferentialCrossSection." + desc: "Differential cross section in pb: matrix_element * pdf1 * pdf2 / (2 e_cm2 x1^2 x2^2), with the unit conversion from GeV^-2. See @ref DifferentialCrossSection." two_body_decay_com: inputs: @@ -2115,12 +2115,12 @@ invariants_from_momenta: desc: "external four-momenta" - name: factors type: [float, single, m, n] - desc: "coefficient matrix selecting the invariant combinations" + desc: "coefficient matrix, one row of per-particle coefficients for each invariant" outputs: - name: invariants type: [float, m] - desc: "the requested invariant combinations" - desc: "Linear combinations of the pairwise invariants built from the external momenta. Building block of @ref PropagatorChannelWeights." + desc: "squared invariant mass of the coefficient-weighted sum of external momenta, one per row" + desc: "For each row of factors, the Minkowski square of the sum of the external momenta weighted by that row. Building block of @ref PropagatorChannelWeights." sde2_channel_weights: inputs: @@ -2135,12 +2135,12 @@ sde2_channel_weights: desc: "propagator widths per channel" - name: indices type: [int, single, c, n] - desc: "invariant index feeding each propagator" + desc: "index into invariants for each propagator, -1 for an unused slot" outputs: - name: channel_weights type: [float, c] desc: "normalized weight per channel" - desc: "Single-diagram-enhanced channel weights from propagator invariants. Building block of @ref PropagatorChannelWeights." + desc: "Channel weights proportional to the inverse product of Breit-Wigner denominators (s - m^2)^2 + (m w)^2 over each channel's propagators, normalized to sum to one. Building block of @ref PropagatorChannelWeights." subchannel_weights: inputs: @@ -2155,18 +2155,18 @@ subchannel_weights: desc: "propagator widths per channel" - name: indices type: [int, single, c, n] - desc: "invariant index feeding each propagator" + desc: "index into invariants for each propagator, -1 for an unused slot" - name: on_shell type: [int, single, c, n] - desc: "flag marking on-shell propagators per channel" + desc: "flag marking on-shell propagators per channel (0: propagator is ignored)" - name: group_sizes type: [int, single, g] desc: "number of subchannels in each channel group" outputs: - name: channel_weights type: [float, c] - desc: "normalized weight per subchannel" - desc: "Channel weights refined into subchannel weights using on-shell information. Building block of @ref SubchannelWeights." + desc: "weight per subchannel, normalized to sum to one within each group" + desc: "Subchannel weights from the product over flagged propagators of (m^4 + (m w)^2) / ((s - m^2)^2 + (m w)^2), normalized within each group of subchannels. Building block of @ref SubchannelWeights." apply_subchannel_weights: inputs: @@ -2186,27 +2186,27 @@ apply_subchannel_weights: - name: channel_weights_out type: [float, d] desc: "weight per diagram" - desc: "Combines channel and subchannel weights into a per-diagram weight. Building block of @ref SubchannelWeights." + desc: "Per-diagram weight: the channel weight times the subchannel weight, with a subchannel index of -1 meaning a subchannel weight of 1. Building block of @ref SubchannelWeights." compress_channel_weights: inputs: - name: channel_index type: [int] - desc: "index of the current channel" + desc: "index of the current channel, always kept" - name: channel_weights type: [float, n] desc: "dense weight per channel" - name: keep_count type: [size, k] - desc: "number of channels to keep" + desc: "number of entries k to keep" outputs: - name: chan_weight_values type: [float, k] - desc: "the kept weights" + desc: "the kept weights; the weight of the current channel is last, unused slots are 0" - name: chan_weight_indices type: [int, k] - desc: "channel index of each kept weight" - desc: "Keeps only the largest channel weights, returning them as a sparse list. Building block of @ref PropagatorChannelWeights." + desc: "channel index of each kept weight, -1 for unused slots" + desc: "Keeps the weight of the current channel plus the k-1 largest other weights, returning them as a sparse list. Building block of @ref PropagatorChannelWeights." restore_channel_weights: inputs: @@ -2223,7 +2223,7 @@ restore_channel_weights: - name: channel_weights type: [float, n] desc: "dense weight per channel" - desc: "Inverse of `compress_channel_weights`: expands the sparse weights back to a dense vector. Building block of @ref PropagatorChannelWeights." + desc: "Expands the sparse weights back to a dense vector, adds a floor of 1e-12 to every channel and renormalizes to sum one (inverse of `compress_channel_weights` up to that). Building block of @ref PropagatorChannelWeights." pt_eta_phi_x: inputs: @@ -2239,8 +2239,8 @@ pt_eta_phi_x: outputs: - name: output type: [float, 3n+2] - desc: "flattened (pt, eta, phi) per final-state particle plus the two momentum fractions" - desc: "Converts external momenta to the transverse-momentum, pseudorapidity, azimuth and momentum-fraction parametrization. Building block of @ref ChiliMapping." + desc: "x1, x2, then (log pt, phi, eta) for each final-state particle" + desc: "Converts external momenta to the parametrization of the beam momentum fractions and per-particle log transverse momentum, azimuth and pseudorapidity. Building block of @ref ChiliMapping." mirror_momenta: inputs: @@ -2249,12 +2249,12 @@ mirror_momenta: desc: "external four-momenta" - name: mirror type: [int] - desc: "per-event flag selecting whether to reflect" + desc: "per-event flag, 1 to mirror, 0 to leave unchanged" outputs: - name: p_mirror type: [float, n, 4] - desc: "the possibly reflected momenta" - desc: "Optionally reflects the momenta along the beam axis to symmetrize the final state." + desc: "the possibly mirrored momenta" + desc: "Flips the sign of the y and z components of all momenta if mirror is 1." momenta_to_x1x2: inputs: @@ -2271,7 +2271,7 @@ momenta_to_x1x2: - name: x2 type: [float] desc: "momentum fraction of beam 2" - desc: "Recovers the beam momentum fractions from a set of external momenta." + desc: "Beam momentum fractions x1 = 2 E_1 / e_cm and x2 = 2 E_2 / e_cm from the energies of the first two (incoming) momenta." boost_to_frame: inputs: @@ -2285,7 +2285,7 @@ boost_to_frame: - name: p_frame type: [float, n, 4] desc: "all momenta boosted into that rest frame" - desc: "Boosts the external momenta into the rest frame of the selected particles, the frame the run card's me_frame asks the matrix element to be evaluated in; a single selected particle comes out exactly at rest. See @ref MatrixElement." + desc: "Boosts the external momenta into the rest frame of the selected particles, the frame the run card's me_frame asks the matrix element to be evaluated in; a single selected particle comes out exactly at rest. If no particle is selected the momenta are passed through unchanged. See @ref MatrixElement." --- @@ -2308,8 +2308,8 @@ uniform_invariant: desc: "sampled invariant" - name: gs type: [float] - desc: "sampling density at s" - desc: "Samples a propagator invariant flat in s. Building block of @ref Invariant." + desc: "Jacobian ds/dr" + desc: "Samples a propagator invariant uniformly in [s_min, s_max]. Building block of @ref Invariant." uniform_invariant_inverse: inputs: @@ -2328,7 +2328,7 @@ uniform_invariant_inverse: desc: "random number in [0, 1]" - name: gs type: [float] - desc: "sampling density at s" + desc: "Jacobian dr/ds" desc: "Inverse of `uniform_invariant`. Building block of @ref Invariant." breit_wigner_invariant: @@ -2354,7 +2354,7 @@ breit_wigner_invariant: desc: "sampled invariant" - name: gs type: [float] - desc: "sampling density at s" + desc: "Jacobian ds/dr" desc: "Samples a propagator invariant with a Breit-Wigner density around the pole mass. Building block of @ref Invariant." breit_wigner_invariant_inverse: @@ -2380,7 +2380,7 @@ breit_wigner_invariant_inverse: desc: "random number in [0, 1]" - name: gs type: [float] - desc: "sampling density at s" + desc: "Jacobian dr/ds" desc: "Inverse of `breit_wigner_invariant`. Building block of @ref Invariant." flat_window_breit_wigner_invariant: @@ -2409,8 +2409,8 @@ flat_window_breit_wigner_invariant: desc: "sampled invariant" - name: gs type: [float] - desc: "sampling density at s" - desc: "Samples a propagator invariant with a Breit-Wigner density whose peak is flattened to a constant inside |sqrt(s) - mass| < window * width (the $-excluded region). Building block of @ref Invariant." + desc: "Jacobian ds/dr" + desc: "Samples a propagator invariant with a Breit-Wigner density whose peak is flattened to a constant (the mean of its two edge values) inside |sqrt(s) - mass| < window * width (the $-excluded region). Building block of @ref Invariant." flat_window_breit_wigner_invariant_inverse: inputs: @@ -2438,7 +2438,7 @@ flat_window_breit_wigner_invariant_inverse: desc: "random number in [0, 1]" - name: gs type: [float] - desc: "sampling density at s" + desc: "Jacobian dr/ds" desc: "Inverse of `flat_window_breit_wigner_invariant`. Building block of @ref Invariant." stable_invariant: @@ -2461,8 +2461,8 @@ stable_invariant: desc: "sampled invariant" - name: gs type: [float] - desc: "sampling density at s" - desc: "Samples a propagator invariant with a 1/s power law around a stable pole. Building block of @ref Invariant." + desc: "Jacobian ds/dr" + desc: "Samples a propagator invariant with a 1/(s - m^2 + 0.01) density, the pole shifted slightly below m^2. Building block of @ref Invariant." stable_invariant_inverse: inputs: @@ -2484,7 +2484,7 @@ stable_invariant_inverse: desc: "random number in [0, 1]" - name: gs type: [float] - desc: "sampling density at s" + desc: "Jacobian dr/ds" desc: "Inverse of `stable_invariant`. Building block of @ref Invariant." stable_invariant_nu: @@ -2510,8 +2510,8 @@ stable_invariant_nu: desc: "sampled invariant" - name: gs type: [float] - desc: "sampling density at s" - desc: "Samples a propagator invariant with a tunable 1/s^nu power law. Building block of @ref Invariant." + desc: "Jacobian ds/dr" + desc: "Samples a propagator invariant with a (s - m^2 + 0.01)^-nu density, the pole shifted slightly below m^2. Building block of @ref Invariant." stable_invariant_nu_inverse: inputs: @@ -2536,7 +2536,7 @@ stable_invariant_nu_inverse: desc: "random number in [0, 1]" - name: gs type: [float] - desc: "sampling density at s" + desc: "Jacobian dr/ds" desc: "Inverse of `stable_invariant_nu`. Building block of @ref Invariant." --- @@ -2686,7 +2686,7 @@ cut_unphysical: - name: w_out type: [float] desc: "weight, set to zero for unphysical events" - desc: "Zeroes the weight of events with unphysical momenta or momentum fractions. See @ref Cuts." + desc: "Zeroes the weight of events with NaN weight or momenta, or with a momentum fraction outside [0, 1]. See @ref Cuts." cut_one: inputs: @@ -2702,8 +2702,8 @@ cut_one: outputs: - name: w type: [float] - desc: "weight, zero when the observable is outside [min, max]" - desc: "Zeroes the weight when a single observable falls outside its window. See @ref Cuts." + desc: "1 if the observable is inside [min, max], 0 otherwise" + desc: "Cut indicator for a single observable: 1 inside the window, 0 outside. See @ref Cuts." cut_all: inputs: @@ -2719,8 +2719,8 @@ cut_all: outputs: - name: w type: [float] - desc: "weight, zero unless all observables are in [min, max]" - desc: "Zeroes the weight unless every observable in the vector is inside its window. See @ref Cuts." + desc: "1 if all observables are in [min, max], 0 otherwise" + desc: "Cut indicator: 1 only if every observable in the vector is inside the window, 0 otherwise. See @ref Cuts." cut_any: inputs: @@ -2736,8 +2736,8 @@ cut_any: outputs: - name: w type: [float] - desc: "weight, zero unless some observable is in [min, max]" - desc: "Zeroes the weight unless at least one observable in the vector is inside its window. See @ref Cuts." + desc: "1 if at least one observable is in [min, max], 0 otherwise" + desc: "Cut indicator: 1 if at least one observable in the vector is inside the window, 0 otherwise. See @ref Cuts." --- title: Energy scale @@ -2746,45 +2746,45 @@ scale_transverse_energy: inputs: - name: momenta type: [float, n, 4] - desc: "final-state momenta" + desc: "external momenta, the two incoming ones first" outputs: - name: scale type: [float] desc: "the energy scale" - desc: "Dynamical scale set to the summed transverse energy of the final state. See @ref EnergyScale." + desc: "Dynamical scale set to the sum of E sin(theta) over the final-state particles (momenta from index 2 on). See @ref EnergyScale." scale_transverse_mass: inputs: - name: momenta type: [float, n, 4] - desc: "final-state momenta" + desc: "external momenta, the two incoming ones first" outputs: - name: scale type: [float] desc: "the energy scale" - desc: "Dynamical scale set to the summed transverse mass of the final state. See @ref EnergyScale." + desc: "Dynamical scale set to the sum of sqrt(E^2 - pz^2) over the final-state particles (momenta from index 2 on). See @ref EnergyScale." scale_half_transverse_mass: inputs: - name: momenta type: [float, n, 4] - desc: "final-state momenta" + desc: "external momenta, the two incoming ones first" outputs: - name: scale type: [float] desc: "the energy scale" - desc: "Dynamical scale set to half the summed transverse mass of the final state. See @ref EnergyScale." + desc: "Dynamical scale set to half the sum of sqrt(E^2 - pz^2) over the final-state particles (momenta from index 2 on). See @ref EnergyScale." scale_partonic_energy: inputs: - name: momenta type: [float, n, 4] - desc: "final-state momenta" + desc: "external momenta, the two incoming ones first" outputs: - name: scale type: [float] desc: "the energy scale" - desc: "Dynamical scale set to the partonic center-of-mass energy. See @ref EnergyScale." + desc: "Dynamical scale set to sqrt(E^2 - pz^2) of the sum of the first two (incoming) momenta. See @ref EnergyScale." mlm_clustering_hadronic: inputs: @@ -3048,7 +3048,7 @@ chili_forward: desc: "mass of each final-state particle" - name: pt_min type: [float, single, n] - desc: "minimum transverse momentum per final-state particle" + desc: "minimum transverse momentum per final-state particle (the last entry is not used)" - name: y_max type: [float, single, n] desc: "maximum rapidity per final-state particle" @@ -3074,7 +3074,7 @@ chili_inverse: desc: "mass of each final-state particle" - name: pt_min type: [float, single, n] - desc: "minimum transverse momentum per final-state particle" + desc: "minimum transverse momentum per final-state particle (the last entry is not used)" - name: y_max type: [float, single, n] desc: "maximum rapidity per final-state particle" @@ -3207,7 +3207,7 @@ leaky_relu: desc: "activated tensor" dims: 0 differentiable: True - desc: "Element-wise leaky ReLU activation. Building block of @ref MLP." + desc: "Element-wise leaky ReLU activation with negative slope 0.01. Building block of @ref MLP." elu: inputs: @@ -3220,7 +3220,7 @@ elu: desc: "activated tensor" dims: 0 differentiable: True - desc: "Element-wise ELU activation. Building block of @ref MLP." + desc: "Element-wise ELU activation, x for x >= 0 and exp(x) - 1 otherwise. Building block of @ref MLP." gelu: inputs: @@ -3233,7 +3233,7 @@ gelu: desc: "activated tensor" dims: 0 differentiable: True - desc: "Element-wise GELU activation. Building block of @ref MLP." + desc: "Element-wise GELU activation, x Phi(x) with the exact Gaussian CDF. Building block of @ref MLP." sigmoid: inputs: @@ -3261,7 +3261,7 @@ scaled_tanh: desc: "activated tensor" dims: 0 differentiable: True - desc: "Element-wise scaled hyperbolic tangent activation. Building block of @ref MLP." + desc: "Element-wise scaled hyperbolic tangent activation, 4 tanh(x / 4). Building block of @ref MLP." softplus: inputs: @@ -3274,25 +3274,25 @@ softplus: desc: "activated tensor" dims: 0 differentiable: True - desc: "Element-wise softplus activation. Building block of @ref MLP." + desc: "Element-wise softplus activation, log(1 + exp(x)). Building block of @ref MLP." rqs_reshape: inputs: - name: input - desc: "raw spline parameters from the subnetwork" + desc: "raw network output with dim * (3 * bin_count + 1) values per event" - name: bin_count desc: "number of spline bins" outputs: - name: widths - desc: "normalized bin widths" + desc: "unnormalized bin widths, shape (batch, dim, bin_count)" - name: heights - desc: "normalized bin heights" + desc: "unnormalized bin heights, shape (batch, dim, bin_count)" - name: derivatives - desc: "knot derivatives" + desc: "unnormalized knot derivatives, shape (batch, dim, bin_count + 1)" class: RqsReshapeInstruction differentiable: True custom_op: True - desc: "Reshapes raw network outputs into rational-quadratic-spline bin widths, heights and derivatives. Building block of @ref Flow." + desc: "Splits raw network outputs per dimension into rational-quadratic-spline bin widths, heights and knot derivatives (no normalization applied). Building block of @ref Flow." rqs_find_bin: inputs: @@ -3301,20 +3301,20 @@ rqs_find_bin: desc: "values to be transformed" - name: in_sizes type: [float, n, b] - desc: "cumulative input-side bin sizes" + desc: "relative input-side bin sizes (not cumulative)" - name: out_sizes type: [float, n, b] - desc: "cumulative output-side bin sizes" + desc: "relative output-side bin sizes (not cumulative)" - name: derivatives type: [float, n, b+1] - desc: "knot derivatives" + desc: "unnormalized knot derivatives" outputs: - name: condition type: [float, n, 6] - desc: "packed spline parameters for the active bin" + desc: "(width, height, cumulative width, cumulative height, left and right unnormalized derivative) of the active bin" differentiable: True dims: 2 - desc: "Builds the per-element spline condition by locating the active bin. Building block of @ref Flow." + desc: "Locates the bin containing each input in [0, 1] and packs its spline parameters; the input is clamped to [0, 1] for the lookup. Building block of @ref Flow." rqs_forward: inputs: @@ -3333,7 +3333,7 @@ rqs_forward: desc: "per-element Jacobian" differentiable: True dims: 2 - desc: "Applies a rational-quadratic spline in the forward direction. Building block of @ref Flow." + desc: "Applies the rational-quadratic spline in the forward direction; inputs outside [0, 1] are passed through with Jacobian 1. Building block of @ref Flow." rqs_inverse: inputs: @@ -3352,7 +3352,7 @@ rqs_inverse: desc: "per-element Jacobian" differentiable: True dims: 2 - desc: "Applies a rational-quadratic spline in the inverse direction. Building block of @ref Flow." + desc: "Applies the rational-quadratic spline in the inverse direction; inputs outside [0, 1] are passed through with Jacobian 1. Building block of @ref Flow." softmax: inputs: @@ -3382,7 +3382,7 @@ softmax_prior: desc: "normalized probabilities" backward_arg: True differentiable: True - desc: "Softmax over the last dimension reweighted by a prior. Building block of @ref DiscreteSampler." + desc: "Softmax over the last dimension with each exp(input) multiplied by the prior before normalization. Building block of @ref DiscreteSampler." --- title: Discrete sampling @@ -3398,10 +3398,10 @@ sample_discrete: outputs: - name: output type: [int] - desc: "sampled index" + desc: "sampled index, floor(r * option_count)" - name: det type: [float] - desc: "probability of the sampled index" + desc: "inverse probability of the sampled index, i.e. option_count" desc: "Samples a uniform discrete choice from a random number. Building block of @ref DiscreteSampler." sample_discrete_inverse: @@ -3418,8 +3418,8 @@ sample_discrete_inverse: desc: "random number in [0, 1]" - name: det type: [float] - desc: "probability of the index" - desc: "Inverse of `sample_discrete`. Building block of @ref DiscreteSampler." + desc: "probability of the index, 1 / option_count" + desc: "Inverse of `sample_discrete`; r is the midpoint of the interval belonging to the index. Building block of @ref DiscreteSampler." sample_discrete_probs: inputs: @@ -3432,11 +3432,11 @@ sample_discrete_probs: outputs: - name: output type: [int] - desc: "sampled index" + desc: "sampled index; choices with zero probability are never sampled" - name: det type: [float] - desc: "probability of the sampled index" - desc: "Samples a discrete choice from a given probability vector. Building block of @ref DiscreteFlow." + desc: "inverse probability of the sampled index" + desc: "Samples a discrete choice from a probability vector (normalized internally). Building block of @ref DiscreteFlow." sample_discrete_probs_inverse: inputs: @@ -3454,7 +3454,7 @@ sample_discrete_probs_inverse: type: [float] desc: "probability of the index" differentiable: True - desc: "Inverse of `sample_discrete_probs`. Building block of @ref DiscreteFlow." + desc: "Inverse of `sample_discrete_probs`; r is the midpoint of the interval belonging to the index. Building block of @ref DiscreteFlow." discrete_histogram: inputs: @@ -3608,9 +3608,9 @@ quantile: outputs: - name: output type: [float, single] - desc: "the estimated quantile" + desc: "the value at position floor(q * batch size), clamped to the last, in the sorted batch; 0 for an empty batch" custom_op: True - desc: "Estimates a quantile of a value over the batch." + desc: "Empirical quantile of a value over the batch, read off the sorted values." one_hot: inputs: @@ -3637,9 +3637,9 @@ madnis_abs_weight: outputs: - name: w type: [float] - desc: "absolute weight f / q" + desc: "absolute weight |f| / q" differentiable: True - desc: "Absolute integrand contribution used by the MadNIS loss. Building block of @ref MadnisLoss." + desc: "Absolute weight |f| / q used by the MadNIS loss. Building block of @ref MadnisLoss." madnis_softclip: inputs: @@ -3660,7 +3660,7 @@ madnis_softclip: type: [float] desc: "softly clipped integrand" differentiable: True - desc: "Softly clips the integrand to limit large weights during training. Building block of @ref MadnisLoss." + desc: "Softly clips the integrand to c asinh(f / c) with c = q * norm * threshold, limiting large weights during training. Building block of @ref MadnisLoss." madnis_variance: inputs: @@ -3679,9 +3679,9 @@ madnis_variance: outputs: - name: w type: [float] - desc: "per-event variance contribution" + desc: "per-event variance contribution g (f / g - mean)^2 / q" differentiable: True - desc: "Per-event contribution to the variance loss. Building block of @ref MadnisLoss." + desc: "Per-event contribution g (f / g - mean)^2 / q to the variance loss. Building block of @ref MadnisLoss." madnis_single_channel_variance: inputs: @@ -3697,7 +3697,7 @@ madnis_single_channel_variance: type: [float] desc: "the variance loss" differentiable: True - desc: "Single-channel variance loss from its running moments. Building block of @ref MadnisLoss." + desc: "Single-channel variance loss var / abs_mean^2. Building block of @ref MadnisLoss." madnis_multi_channel_variance: inputs: @@ -3712,7 +3712,7 @@ madnis_multi_channel_variance: type: [float] desc: "the variance loss" differentiable: True - desc: "Multi-channel variance loss summed over channels. Building block of @ref MadnisLoss." + desc: "Multi-channel variance loss (sum_c sqrt(var_c) / sum_c abs_mean_c)^2. Building block of @ref MadnisLoss." --- title: Indexing @@ -3795,10 +3795,10 @@ unweight: - name: indices desc: "batch indices of the accepted events" - name: uw_weights - desc: "residual weights of the accepted events" + desc: "weights of the accepted events, max(|w|, max_weight) with the sign of w" class: UnweightInstruction custom_op: True - desc: "Accept/reject unweighting against a maximum weight. See @ref Unweighter." + desc: "Accept/reject unweighting: an event is accepted with probability |w| / max_weight. See @ref Unweighter." --- title: VEGAS @@ -3853,12 +3853,12 @@ vegas_histogram: outputs: - name: values type: [float, single, n, b] - desc: "summed weight per bin and dimension" + desc: "summed squared weight per bin and dimension" - name: counts type: [int, single, n, b] desc: "event count per bin and dimension" custom_op: True - desc: "Accumulates the weighted histograms VEGAS uses to refine its grid. Building block of @ref VegasHistogram." + desc: "Accumulates, per dimension, the squared weights and event counts in equal-width bins of the unit interval, as used to refine the VEGAS grid. Points outside [0, 1) are skipped. Building block of @ref VegasHistogram." histogram: inputs: @@ -3880,9 +3880,9 @@ histogram: outputs: - name: values type: [float, single, b+2] - desc: "summed weight per bin, with underflow and overflow" + desc: "summed weight per bin; first entry is underflow, last is overflow" - name: square_values type: [float, single, b+2] desc: "summed squared weight per bin, for the statistical error" custom_op: True - desc: "Accumulates a weighted histogram with underflow and overflow bins. See @ref Histogram." + desc: "Accumulates a weighted histogram over equal-width bins between min and max, with underflow and overflow bins. See @ref Histogram." diff --git a/madspace/src/cpu/device.hpp b/madspace/src/cpu/device.hpp index c6de22e360..fab6138ba5 100644 --- a/madspace/src/cpu/device.hpp +++ b/madspace/src/cpu/device.hpp @@ -100,7 +100,6 @@ class AsyncCpuDevice : public CpuDevice { if (batch_size == 0) { return {0, 0}; } - // return {1, batch_size}; if (single_job) { return {1, batch_size}; } @@ -108,7 +107,6 @@ class AsyncCpuDevice : public CpuDevice { std::size_t batch_size_vec = (batch_size + simd_vec_size - 1) / simd_vec_size; std::size_t min_batch_size = 64 / simd_vec_size; std::size_t thread_count = _thread_pool.thread_count(); - // std::size_t job_count = (batch_size + min_batch_size - 1) / min_batch_size; std::size_t job_count = batch_size_vec < thread_count * min_batch_size ? (batch_size_vec + min_batch_size - 1) / min_batch_size : thread_count; @@ -138,7 +136,6 @@ class AsyncCpuDevice : public CpuDevice { if (_barrier_state) { _funcs_after_barrier.push_back(job_func); } else { - // _thread_pool.submit(job_func); jobs.push_back(job_func); } } diff --git a/madspace/src/cpu/runtime.cpp b/madspace/src/cpu/runtime.cpp index 5685f7d5f5..67e0b0f65b 100644 --- a/madspace/src/cpu/runtime.cpp +++ b/madspace/src/cpu/runtime.cpp @@ -54,11 +54,8 @@ using namespace madspace::kernels; namespace { -// External tensors (e.g. row-major numpy arrays, or expanded gradients from -// torch) can have a batch stride other than one. The vectorized kernels only -// handle batch stride one and leave everything else to the scalar kernels, which -// round slightly differently, so copy them once to make the results independent -// of the memory layout of the arguments. +// Copy tensors with batch stride other than one (e.g. external numpy or torch +// data), so that the results do not depend on the vectorized/scalar kernel choice. TensorVec batch_contiguous(const TensorVec& tensors) { TensorVec ret; ret.reserve(tensors.size()); @@ -86,9 +83,8 @@ void op_matrix_element( std::vector input_keys(input_count); std::vector output_keys(output_count); std::vector input_ptrs(input_count), output_ptrs(output_count); - // Only prepare the inputs if the matrix element is actually called: making an - // input contiguous can start an asynchronous copy, whose target would be freed - // while the copy is still running if we returned early below. + // Prepare the inputs only if the matrix element is called: an asynchronous copy + // must not be abandoned by the early return below. bool call_matrix_element = me_index != 0xBADCAFE && batch_size > 0; for (std::size_t i = 0; i < input_count && call_matrix_element; ++i) { input_keys[i] = static_cast( @@ -113,8 +109,7 @@ void op_matrix_element( output = Tensor(instruction.output_dtypes[i], shape, device); output_ptrs[i] = output.data(); if (me_index == 0xBADCAFE) { - // flat dummy matrix element for testing purposes - // implemented at LUMI hackathon where the coffee was indeed terrible + // flat dummy matrix element for testing switch (output_keys[i]) { case UMAMI_OUT_MATRIX_ELEMENT: device.submit([=]() mutable { @@ -221,17 +216,14 @@ void backward_op_matmul( if (!input_grad) { input_grad = Tensor(DataType::dt_float, input.shape(), device, AllocHint::local_grad); - // input_grad.zero(device); } if (!weight_grad) { weight_grad = Tensor(DataType::dt_float, weight.shape(), device, AllocHint::local_grad); - // weight_grad.zero(device); } if (!bias_grad) { bias_grad = Tensor(DataType::dt_float, {1, dims_out}, device, AllocHint::local_grad); - // bias_grad.zero(device); } if (batch_size == 0) { return; @@ -554,7 +546,6 @@ void batch_reduce_sum_mean_backward_impl( if (!input_grad) { input_grad = Tensor(DataType::dt_float, input.shape(), device, AllocHint::local_grad); - // input_grad.zero(device); } device.sync_barrier(); @@ -1385,7 +1376,6 @@ std::pair CpuRuntime::run_backward_single( Tensor all_global_grads( DataType::dt_float, {_grad_global_total_size}, AllocHint::global_grad ); - // all_global_grads.zero(); TensorVec global_grads = all_global_grads.split_and_reshape(_grad_global_shapes); for (auto [index, grad] : zip(_grad_global_indices, global_grads)) { if (local_grads[index]) { @@ -1409,7 +1399,6 @@ std::pair CpuRuntime::run_backward_single( device, AllocHint::local_grad ); - // grad.zero(device); } } using DeviceType = CpuDevice; @@ -1576,7 +1565,6 @@ std::pair CpuRuntime::run_backward_concurrent( Tensor all_global_grads( DataType::dt_float, {_grad_global_total_size}, AllocHint::global_grad ); - // all_global_grads.zero(); TensorVec global_grads = all_global_grads.split_and_reshape(_grad_global_shapes); for (auto [index, grad] : zip(_grad_global_indices, global_grads)) { if (local_grads[index]) { @@ -1620,7 +1608,6 @@ std::pair CpuRuntime::run_backward_concurrent( device, AllocHint::local_grad ); - // grad.zero(device); } } diff --git a/madspace/src/cpu/tensor.hpp b/madspace/src/cpu/tensor.hpp index 0eeba4ba86..c460ea4f93 100644 --- a/madspace/src/cpu/tensor.hpp +++ b/madspace/src/cpu/tensor.hpp @@ -406,15 +406,9 @@ inline void tensor_foreach_impl( bool single_job, S... scalar_args ) { - // A broadcast *output* (size(0) != batch_size, e.g. a global's gradient - // accumulator during backward) is shared across the whole batch instead of - // having one slot per batch element. Splitting the batch across worker threads - // would then have multiple threads accumulate into the very same memory - // concurrently with no synchronization, so force a single job in that case - // (the whole batch handled sequentially by one thread). Broadcast *inputs* are - // only ever read, never written, in both forward and backward -- concurrent - // reads of the same memory are safe, so they don't need this. - // SIMD lanes would also write to the same memory, so use the scalar kernel. + // A broadcast output (size(0) != batch_size, e.g. a global's gradient + // accumulator) is shared by the whole batch: run a single job with the + // scalar kernel to avoid concurrent or lane-parallel accumulation. bool broadcast_output = false; for (const Tensor* out : outputs) { if (out->size(0) != batch_size) { @@ -435,11 +429,9 @@ inline void tensor_foreach_impl( ) ); - // The vectorized kernel loads and stores the lanes of a vector with plain - // contiguous memory accesses. It is only used if every batched argument has - // batch stride one (or is a literal padded to simd_vec_size lanes); single - // inputs are read with broadcast loads and can have any layout. Otherwise - // the whole batch is handled by the scalar kernel. + // The vectorized kernel needs batch stride one (or a padded literal) for every + // batched argument; single inputs can have any layout. Otherwise use the + // scalar kernel for the whole batch. constexpr std::size_t n_args = n_in + n_out + sizeof...(S); std::array vec_steps{}; bool vectorize = !broadcast_output; diff --git a/madspace/src/driver/adam_optimizer.cpp b/madspace/src/driver/adam_optimizer.cpp index 7e05fe7430..76f37f2c3c 100644 --- a/madspace/src/driver/adam_optimizer.cpp +++ b/madspace/src/driver/adam_optimizer.cpp @@ -106,7 +106,6 @@ TensorVec AdamOptimizer::step(const TensorVec& inputs) { if (_grad_clipper) { auto clip_result = _grad_clipper->run({global_grads.at(0), _threshold_tensor}); global_grads = {clip_result.at(0)}; - // double norm = clip_result.at(1).cpu().view()[0]; } device->adam_step( diff --git a/madspace/src/driver/backend.cpp b/madspace/src/driver/backend.cpp index 4edc7ddb53..88766738ff 100644 --- a/madspace/src/driver/backend.cpp +++ b/madspace/src/driver/backend.cpp @@ -26,11 +26,8 @@ struct LoadedBackend { #endif shared_lib = std::shared_ptr( dlopen(std::format("{}/{}.{}", lib_path, file, so_ext).c_str(), RTLD_NOW), - // Unlike std::unique_ptr, std::shared_ptr's deleter is invoked - // unconditionally on destruction, even when the stored pointer is - // null -- if dlopen() above fails, the throw a few lines down - // would otherwise unwind through a dlclose(nullptr), which - // segfaults instead of letting the runtime_error propagate. + // shared_ptr calls the deleter even for a null pointer, and + // dlclose(nullptr) would crash when dlopen fails. [](void* lib) { if (lib) dlclose(lib); } diff --git a/madspace/src/driver/channel_generator.cpp b/madspace/src/driver/channel_generator.cpp index 0006d2c968..8b33cdf600 100644 --- a/madspace/src/driver/channel_generator.cpp +++ b/madspace/src/driver/channel_generator.cpp @@ -64,8 +64,8 @@ DerivedSeed::SeedType unweight_seed_type(bool is_survey, std::size_t survey_pass : DerivedSeed::second_survey_unweight; } -// Role folded into job_index so integrand_channel and integrand_common get -// independent streams despite sharing seed_type/channel_index. +// Folded into job_index so integrand_channel and integrand_common get independent +// streams. enum RngRole { rng_role_channel = 0, rng_role_common = 1, rng_role_count = 2 }; DerivedSeed generate_phase_seed( @@ -401,9 +401,8 @@ void ChannelEventGenerator::integrate(const GeneratorBatchJob& job) { if (w_view[i] != 0) { ++sample_count_after_cuts; } - // With a bias b the weights are w b: the absolute integral, which drives - // the adaptation, unweighting and channel allocation, keeps the bias, - // while the reported cross section divides it out. + // With a bias b the weights are w b: the absolute integral keeps the bias, + // the reported cross section divides it out. double weight = w_view[i]; if (_field_indices.bias != -1 && weight != 0.) { weight /= job.biases.view()[i]; @@ -428,8 +427,7 @@ void ChannelEventGenerator::integrate(const GeneratorBatchJob& job) { auto hist2_view = job.hists.at(2 * i + 1).view()[0]; auto& chan_hist = _histograms.at(i); for (std::size_t j = 0; j < hist_view.size(); ++j) { - // note: we don't assign the actuals means and errors here. There are - // still some normalization steps necessary that are performed later + // raw sums; normalized in EventGenerator::histograms() chan_hist.bin_values.at(j) += hist_view[j]; chan_hist.bin_errors.at(j) += hist2_view[j]; } @@ -493,9 +491,8 @@ double ChannelEventGenerator::channel_weight_sum(std::size_t event_count) { return weight_sum; } -// Assigns job's base seed on the scheduling thread, from its logical identity -// only (channel, kind, sequence) -- never from context_index -- so it doesn't -// depend on worker/context assignment. +// The job's seed depends only on channel, kind and sequence, not on the context +// it runs on. void ChannelEventGenerator::start_job( GeneratorBatchJob& job, ResultQueue& result_queue, @@ -507,8 +504,7 @@ void ChannelEventGenerator::start_job( job.rng_is_survey = is_survey; job.rng_survey_pass = survey_pass; job.rng_job_index = is_survey ? _survey_rng_seq++ : _generate_rng_seq++; - // Through result_queue, so a job that throws still posts its result and the - // exception reaches the waiting thread instead of leaving it blocked forever. + // Submitted via result_queue so that an exception reaches the waiting thread. result_queue.submit( _contexts.at(job.context_index)->thread_pool(), job.job_id, @@ -530,10 +526,8 @@ void ChannelEventGenerator::start_job( ? _config.cpu_batch_size : _config.gpu_batch_size; std::size_t batch_size = max_batch_size; - // Only VEGAS batches shrink to fit -- they need next_vegas_batch_size()'s - // exact geometric progression for correct grid-adaptation statistics. - // Generation batches always submit full device-sized jobs; a little - // overshoot there is expected and cheaper than a partial job. + // Only VEGAS batches shrink to fit, to keep their batch sizes exact. + // Generation jobs are always full device batches. if (job.is_vegas_batch && job.batch_event_count > 0 && batch_size > job.batch_event_count) { batch_size = job.batch_event_count; @@ -544,8 +538,7 @@ void ChannelEventGenerator::start_job( std::size_t total_count = 0, repetitions = 0; TensorVec all_ps_points; while (true) { - // Successive calls draw further from integrand_channel's own - // persistent stream (set once above) -- no per-call re-seeding. + // successive calls continue the stream seeded above auto ps_points = runtimes.integrand_channel->run({Tensor({batch_size})}); std::size_t acc_count = @@ -607,8 +600,7 @@ void ChannelEventGenerator::start_job( if (_field_indices.bias != -1) { job.biases = job.events.at(_field_indices.bias).cpu(); } - // observable_histograms/vegas_histogram/discrete_histogram don't consume - // random numbers, so they're never seeded. + // the histogram runtimes use no random numbers and are never seeded if (runtimes.observable_histograms) { auto hists = runtimes.observable_histograms->run( {job.events.at(_field_indices.weight), @@ -710,10 +702,8 @@ void ChannelEventGenerator::clear_events() { } void ChannelEventGenerator::update_max_weight(Tensor weights) { - // Freeze on the expected unweighted count of the committed generate stages - // rather than on count_unweighted: the latter is advanced by the unweight - // stages, whose commits interleave with the generate stages depending on - // thread timing, which made the freeze point (and the run) timing-dependent. + // Freeze on _expected_unweighted, which depends only on the generate stages, + // rather than on count_unweighted, which depends on unweight commit timing. if (_max_weight_fixed || _expected_unweighted > _config.freeze_max_weight_after) { return; } @@ -734,8 +724,7 @@ void ChannelEventGenerator::update_max_weight(Tensor weights) { } std::sort(_large_weights.begin(), _large_weights.end(), std::greater{}); apply_truncation_budget(); - // expected accepted events of this job, against the max weight it will be - // unweighted with (see prepare_unweight_job) + // expected accepted events of this job at the max weight it is unweighted with if (_max_weight > 0) { for (std::size_t i = 0; i < w_view.size(); ++i) { _expected_unweighted += std::min(std::abs(w_view[i]) / _max_weight, 1.); @@ -746,11 +735,8 @@ void ChannelEventGenerator::update_max_weight(Tensor weights) { void ChannelEventGenerator::set_target_count(std::size_t target_count) { std::size_t old_target = _status.count_target; _status.count_target = target_count; - // The truncation budget scales with count_target, and _max_weight only - // rises. A cap chosen under a larger target than the channel ends up with - // (e.g. generate()'s round-one seed) would otherwise never be corrected: a - // channel that has met its smaller target is not revisited. Tighten the cap - // now; unweight_all() re-unweights the whole file against it at the end. + // The truncation budget scales with count_target and _max_weight only rises, so + // tighten the cap for a smaller target here; unweight_all() re-unweights later. if (target_count < old_target && _max_weight != 0 && !_max_weight_fixed && _expected_unweighted <= _config.freeze_max_weight_after) { apply_truncation_budget(); diff --git a/madspace/src/driver/event_generator.cpp b/madspace/src/driver/event_generator.cpp index 44625e2558..8178e18a83 100644 --- a/madspace/src/driver/event_generator.cpp +++ b/madspace/src/driver/event_generator.cpp @@ -58,8 +58,7 @@ EventGenerator::EventGenerator( _seed(seed), _status_file(status_file) {} -// Commits a job's generate stage in per-channel dispatch order. Doesn't write -// events itself -- if job.unweight, that's deferred to commit_unweight_job(). +// Commits a job's generate stage. Events are written by commit_unweight_job(). void EventGenerator::commit_generate_job(GeneratorBatchJob& job) { auto& channel = _channels.at(job.channel_index); auto& channel_job_count = _channel_job_counts.at(job.channel_index); @@ -72,7 +71,6 @@ void EventGenerator::commit_generate_job(GeneratorBatchJob& job) { --channel_job_count; --_context_job_counts.at(job.context_index); if (job.unweight) { - // Snapshot max_weight here, at this job's fixed commit position. channel->prepare_unweight_job(job); _context_unweight_queue.at(job.context_index).push_back(job.job_id); _channel_unweight_order.at(job.channel_index).push_back(job.job_id); @@ -118,10 +116,8 @@ void EventGenerator::run_generate(GenerateMode mode) { reset_start_time(); print_gen_init(); - // update_max_weight() scales its truncation budget with count_target, but - // set_target_count() only runs once a round has committed. Seed a generous target - // so round one truncates at all; _max_weight only rises, and set_target_count() - // tightens it when the real (smaller) target arrives. + // Seed a target so the first round has a truncation budget; set_target_count() + // tightens the maximum weight once the real target arrives. for (auto& channel : _channels) { if (channel->status().count_opt == 0 || mode == GenerateMode::fix_max_weights) { channel->set_target_count( @@ -182,7 +178,7 @@ void EventGenerator::run_generate(GenerateMode mode) { std::size_t unweight_dispatched = start_jobs(); std::size_t round_in_flight = (_job_id - job_id_before) + unweight_dispatched; - // Wait for the round to fully commit before resyncing cross-channel fractions. + // Wait for the round to commit before updating the channel fractions. while (round_in_flight > 0) { std::size_t job_id = wait_for_result(round_in_flight); auto& job = _running_jobs.at(job_id); @@ -293,8 +289,7 @@ void EventGenerator::survey(std::size_t survey_pass) { std::size_t job_id = wait_for_result(in_flight); auto& job = _running_jobs.at(job_id); if (job.unweighted_events.size() == 0) { - // Generate-stage completion, globally ordered via - // _ready_gen/_commit_cursor. + // generate-stage completion, committed in global job id order _ready_gen.insert(job_id); while (_ready_gen.erase(_commit_cursor) > 0) { auto& gen_job = _running_jobs.at(_commit_cursor); @@ -309,11 +304,9 @@ void EventGenerator::survey(std::size_t survey_pass) { channel->update_max_weight(gen_job.weights); --channel_job_count; --_context_job_counts.at(gen_job.context_index); - // Global order is ascending job id and so is the per-channel one, - // so this job is necessarily at the front of its channel's deque. + // the global order implies this job is at the front of its deque _channel_gen_order.at(gen_job.channel_index).pop_front(); if (gen_job.unweight) { - // Snapshot max_weight at this fixed commit position. channel->prepare_unweight_job(gen_job); _context_unweight_queue.at(gen_job.context_index) .push_back(gen_job.job_id); @@ -330,7 +323,7 @@ void EventGenerator::survey(std::size_t survey_pass) { ++_commit_cursor; } } else { - // Unweight-stage completion, ordered per channel only. + // unweight-stage completion, committed in per-channel order auto& ready = _channel_unweight_ready.at(job.channel_index); auto& order = _channel_unweight_order.at(job.channel_index); ready.insert(job_id); @@ -365,13 +358,9 @@ void EventGenerator::survey(std::size_t survey_pass) { print_survey_update(true, done_event_count, total_event_count, iter - 1); } -// Every job counted in *in_flight* posts exactly one result (see -// ResultQueue::submit), so this can't block on a job that died. If a job threw, or -// the abort check does while waiting, the other jobs still in flight write into -// _running_jobs: wait them out and drop the bookkeeping of the aborted run before -// rethrowing, so the exception unwinds with no job left behind. The running jobs -// stop at their next cancellation check, and an abort requested while they do -// takes precedence over the job's own error. +// Every job counted in in_flight posts exactly one result (see ResultQueue::submit). +// If a job or the abort check throws, the remaining jobs are waited out and the +// bookkeeping is reset before rethrowing; an abort takes precedence over a job error. std::size_t EventGenerator::wait_for_result(std::size_t& in_flight) { std::exception_ptr exception; try { @@ -430,8 +419,8 @@ std::size_t EventGenerator::next_batch_event_count(std::size_t channel_index) co return compute_generation_batch_event_count( status.count_target, status.count_unweighted, - // Not count_opt: start_jobs() splits batches by nominal size, and a job - // repeating batches to pass cuts yields more raw points than that. + // not count_opt: jobs that repeat batches to pass cuts yield more raw points + // than their nominal size channel->count_requested_opt(), abs_cross_section.count(), abs_cross_section.rel_error(), @@ -464,10 +453,7 @@ std::size_t EventGenerator::start_jobs() { ); // Round-robin through _ready_jobs, one device batch per generation entry per - // visit, so multiple channels' batches interleave instead of one channel's - // batch draining before the next is even touched -- see _ready_job_rr_cursor. - // A VEGAS batch is still dispatched atomically in a single visit: VEGAS needs - // next_vegas_batch_size()'s exact progression. + // visit; a VEGAS batch is dispatched in full in a single visit. while (job_count < target_count && !_ready_jobs.empty()) { if (_ready_job_rr_cursor >= _ready_jobs.size()) { _ready_job_rr_cursor = 0; @@ -493,8 +479,7 @@ std::size_t EventGenerator::start_jobs() { auto& job = std::get<0>(_running_jobs.emplace(_job_id, std::move(new_job))) ->second; - // Records this channel's commit order; drained by survey()/generate() - // -- see _channel_gen_order. + // commit order, see _channel_gen_order _channel_gen_order.at(job.channel_index).push_back(_job_id); _channels.at(job.channel_index) ->start_job(job, _result_queue, _seed, _survey_job, _survey_pass); @@ -510,12 +495,11 @@ std::size_t EventGenerator::start_jobs() { bool fully_dispatched = atomic_dispatch || ready_job.batch_event_count == 0; if (fully_dispatched) { if (!ready_job.is_vegas_batch) { - // Only meaningful for generate(); harmlessly unused by the other - // callers of start_jobs(). + // only used by generate() _channel_batch_dispatch_done.at(ready_job.channel_index) = true; } _ready_jobs.erase(_ready_jobs.begin() + _ready_job_rr_cursor); - // The next entry has shifted into this index -- don't advance past it. + // the next entry shifted into this index } else { ++_ready_job_rr_cursor; } @@ -615,8 +599,8 @@ void EventGenerator::update_integral_status() { } void EventGenerator::update_integral_fractions() { - // With an integral known from an earlier run, the sum of the channel estimates - // normalizes the fractions; without, this is _status.mean_abs. + // With priors from an earlier run, the fractions are normalized by the sum of + // the channel estimates, otherwise by _status.mean_abs. double total_estimate = _status.mean_abs; if (std::any_of(_channels.begin(), _channels.end(), [](auto& channel) { return channel->has_abs_integral_prior(); @@ -839,8 +823,8 @@ void EventGenerator::combine_to_lhe( std::size_t event_count = 0; std::size_t last_update_count = 0; bool done = false; - // Batches are seeded by submission order and written in that same order - // (buffered in ready_slots until their turn), independent of completion timing. + // Batches are seeded and written in submission order (held in ready_slots until + // their turn), independent of completion timing. std::size_t next_batch_seq = 0; std::size_t write_cursor = 0; std::vector slot_batch_seq(buffers.size()); @@ -1142,7 +1126,6 @@ std::unordered_set EventGenerator::used_globals() const { std::vector EventGenerator::channel_status() const { std::vector status; for (auto& channel : _channels) { - // double target_count = channel.integral_fraction * _config.target_count; status.push_back(channel->status()); } return status; @@ -1184,8 +1167,7 @@ EventGenerator::init_combine() { for (auto& channel : _channels) { particle_count = std::max(particle_count, channel->event_file().particle_count()); - // Exact apportioned target (see update_integral_fractions()), so counts - // sum to exactly _config.target_count. + // apportioned target, see update_integral_fractions() std::size_t count = channel->status().count_target; count_sum += count; channel->event_file().seek(0); @@ -1354,8 +1336,7 @@ void EventGenerator::fill_lhe_event( lhe_event.scale = event_in.ren_scale(); lhe_event.alpha_qed = 0; // TODO: populate this lhe_event.alpha_qcd = event_in.alpha_qcd(); - // Both scales, as LHEF3 attributes: SCALUP holds only one number - // and the codes do not agree on which, so write them out explicitly. + // both scales are also written as attributes, see LHEEvent::mu_r int scale_flags = _channels.at(0)->event_layout_extra_flags(); lhe_event.mu_r = event_in.ren_scale(); lhe_event.mu_f1 = (scale_flags & EventRecord::f_beam1) diff --git a/madspace/src/driver/event_histograms.cpp b/madspace/src/driver/event_histograms.cpp index a00376ee51..038b29eb01 100644 --- a/madspace/src/driver/event_histograms.cpp +++ b/madspace/src/driver/event_histograms.cpp @@ -85,7 +85,7 @@ void EventHistograms::fill( groups[subproc].push_back(i); } } - // values[observable][event index in buffer], NaN when not evaluated + // values[spec][event index in buffer], NaN when not evaluated std::vector> values( _specs.size(), std::vector(count, std::nan("")) ); diff --git a/madspace/src/driver/format.cpp b/madspace/src/driver/format.cpp index bae7e921c6..569452c394 100644 --- a/madspace/src/driver/format.cpp +++ b/madspace/src/driver/format.cpp @@ -17,10 +17,8 @@ const std::array progress_symbols{ " ", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█" }; -// Fallback for format_with_error() once the relative error is O(1) or larger (or -// value/error are non-finite/non-positive): the "1.234(56)" digit-of-error notation is -// meaningless there, so print "value ± error" instead, with the error rendered to two -// significant digits (or the value alone if the error itself isn't meaningful). +// Fallback for format_with_error() if the relative error is O(1) or larger or an +// argument is not finite or positive: "value ± error", or the value alone. std::string format_value_pm_error(double value, double error) { if (!std::isfinite(error) || error <= 0.) { if (!std::isfinite(value) || value == 0.) { diff --git a/madspace/src/driver/io.cpp b/madspace/src/driver/io.cpp index d4234ea0f7..42c1c16acc 100644 --- a/madspace/src/driver/io.cpp +++ b/madspace/src/driver/io.cpp @@ -37,15 +37,8 @@ DataType str_to_dtype(std::string dtype) { } } -// The header-length field's width depends on the npy format version: 1.0 -// uses 2 bytes (uint16, so the header itself must fit in 65535 bytes), 2.0/3.0 -// use 4 bytes (uint32) -- write_event_header() switches to 2.0 whenever the -// header would otherwise be too long to describe in 2 bytes (eg several "add -// process" channels of different final-state multiplicity combined into one -// binary EventFile can easily need a header longer than 65535 bytes' worth of -// field descriptors). Only ever reading the 2-byte case silently truncated the -// *declared* length for any such oversized header, cutting a field name off -// mid-string for every reader (this one and numpy alike). +// npy format 1.0 stores the header length in 2 bytes, 2.0/3.0 in 4 bytes; +// write_event_header() uses 2.0 when the header exceeds 65535 bytes. constexpr std::size_t npy_preamble_size = 10; constexpr std::size_t npy_wide_preamble_size = 12; @@ -261,17 +254,7 @@ std::tuple write_event_header( std::size_t header_size = 0 ) { using namespace std::string_literals; - // Built into a string first (rather than written straight to - // file_stream, as this function used to) so its true length is known - // before deciding whether npy format 1.0's 2-byte header-length field - // (max 65535) is wide enough, or format 2.0's 4-byte field is needed - // instead -- see parse_header()'s matching version-selection logic for - // why: silently wrapping an oversized length into 16 bits (the old - // behavior here) corrupts the file for every reader, this project's own - // and numpy's alike. A large particle_count (eg several "add process" - // channels of different final-state multiplicity combined into one - // binary EventFile) easily needs a header longer than 65535 bytes' - // worth of field descriptors. + // Build the content first to know whether format 1.0 or 2.0 is needed. std::string content = "{'descr':["s; auto field_descr = full_descr(particle_count, layout); for (auto [field_name, field_type] : field_descr) { @@ -279,10 +262,8 @@ std::tuple write_event_header( } content += "],'fortran_order':False,'shape':("; - // prefix_len: magic (6) + major/minor version (2) + header-length field - // (2 for v1.0, 4 for v2.0) -- header_size is always the *absolute* file - // offset where the header ends and event data begins (matching - // EventFile's own _header_size-based seek math), regardless of format. + // prefix_len: magic (6) + version (2) + length field (2 or 4). header_size is + // the absolute file offset where the event data begins. bool wide = false; std::size_t prefix_len = 10; if (header_size == 0) { diff --git a/madspace/src/driver/lhe_output.cpp b/madspace/src/driver/lhe_output.cpp index 6a93d5fcbc..350aa8b10e 100644 --- a/madspace/src/driver/lhe_output.cpp +++ b/madspace/src/driver/lhe_output.cpp @@ -166,8 +166,8 @@ void LHEEvent::format_to(std::string& buffer) const { } if (lo_info && lo_info->qcd_power >= 0 && lo_info->has_beam1 && lo_info->has_beam2) { - // as madevent's unwgt.f writes it: the alpha_s power left at mu_R and - // the vertex scales, then per beam n, the n pdgs, x and scales + // madevent's unwgt.f layout: alpha_s power at mu_R, vertex scales, then + // per beam the n pdgs, x values and scales int n_vertices = static_cast(lo_info->asrwt.size()); std::format_to( insert_iter, @@ -289,16 +289,14 @@ void LHECompleter::init_propagator_data( resonant_prop_indices.clear(); resonant_prop_indices.resize(decay_count, -1); - // permutation maps external leg -> topology slot; invert it here, since - // using it directly mismatches whenever it isn't self-inverse. + // permutation maps external leg -> topology slot, so invert it std::vector inv_permutation(permutation.size()); for (std::size_t leg = 0; leg < permutation.size(); ++leg) { inv_permutation.at(permutation.at(leg)) = leg; } - // The permutation runs over all external legs, incoming first, so the - // outgoing ones start at the incoming count -- 2 for a collision, 1 for a - // decay. Getting this wrong silently reads another particle's color flow. + // the permutation runs over all external legs, so outgoing ones start after + // the incoming ones for (auto [index, mass, perm_index] : zip(topo.outgoing_indices(), topo.outgoing_masses(), @@ -327,12 +325,8 @@ void LHECompleter::find_resonant_propagators( std::vector>& prop_colors, std::vector& resonant_prop_indices ) { - // For each decay, the propagators hanging below it with no propagator in - // between. A decay that never becomes a propagator is not written to the - // LHE, so what sits under it belongs to the nearest ancestor that is: its - // mask passes straight through. Without this a resonance separated from - // its parent resonance by a plain internal line keeps mother (1,2) -- the - // W of a q q~ > Z > l l(-> l W) diagram, say. + // For each decay, the mask of propagators below it with none in between. A + // decay that is not a propagator passes its children's mask on to its parent. std::vector subtree_prop_masks(topo.decays().size(), 0); // Pass 1: resonance status from mass/width alone, no color involved. @@ -373,13 +367,11 @@ void LHECompleter::find_resonant_propagators( } if (_propagators.size() == prop_offset) { - // Nothing resonant: no color is needed, and skipping it sidesteps - // color flows that never get validated for non-resonant parts. + // nothing resonant, so no color is needed return; } - // Pass 2: a decay needs its color computed only if it (or something - // further down its own decay chain) is resonant. + // Pass 2: a decay needs its color only if it or a decay below it is resonant. std::vector needed(topo.decays().size(), false); for (auto& decay : topo.decays()) { if (decay.child_indices.size() == 0) { @@ -401,8 +393,7 @@ void LHECompleter::find_resonant_propagators( continue; } - // Prefer this diagram's own pdg (merge_same_topologies can share a - // topology across diagrams with different particles per slot). + // prefer this diagram's own pdg, as topologies can be shared between diagrams int propagator_pdg = decay.pdg_id; if (decay.flat_propagator_index != Topology::no_propagator && decay.flat_propagator_index < propagator_pdgs.size()) { @@ -594,11 +585,7 @@ void LHECompleter::complete_event_data( event.process_id = subproc_data.process_id; - // Number of leading entries in the event record that are initial state: - // 2 for a collision, 1 for a decay. Everything that indexes past the - // initial state -- where resonances get inserted, what the outgoing - // particles' mothers are, how far the momentum masks are shifted -- is - // offset by this rather than by a hard-coded 2. + // number of initial-state entries: 2 for a collision, 1 for a decay const std::size_t n_in = subproc_data.incoming_count; std::size_t color_offset = @@ -623,8 +610,7 @@ void LHECompleter::complete_event_data( } else { particle.status_code = 1; particle.mother1 = 1; - // A decay has a single mother, which LHE spells as - // mother1 == mother2 rather than a (1, 2) range. + // a decay has a single mother: mother1 == mother2 particle.mother2 = static_cast(n_in); } particle.mass = _masses.at(mass_offset + particle_index); @@ -633,9 +619,7 @@ void LHECompleter::complete_event_data( ++particle_index; } - // The flavor tables list the incoming legs in leg order, and a mirrored - // event has them the other way round on the beams. LHE wants particle 1 to - // come from beam 1, moving along +z. + // LHE wants particle 1 to come from beam 1, moving along +z if (n_in == 2 && event.particles.at(0).pz < 0.) { std::swap(event.particles.at(0), event.particles.at(1)); } @@ -671,11 +655,7 @@ void LHECompleter::complete_event_data( momentum_mask >>= 1; } double m2 = e * e - px * px - py * py - pz * pz; - // Once bw_cutoff exceeds mass/width the window reaches below zero, - // where there is no invariant mass left to exclude. Squaring a - // negative m_min would instead turn it into a large positive floor and - // reject nearly everything, so a wider window would write *fewer* - // resonances than a narrow one. + // clamp m_min at zero so that a negative lower edge is not squared double m_min = std::max(0., propagator.mass - _bw_cutoff * propagator.width); double m_max = propagator.mass + _bw_cutoff * propagator.width; if (m2 > m_min * m_min && m2 < m_max * m_max) { @@ -702,12 +682,8 @@ void LHECompleter::complete_event_data( event.particles.insert( event.particles.begin() + n_in, new_particles.rbegin(), new_particles.rend() ); - // Where each propagator ended up among the ones this event actually wrote, - // which is the order they were just inserted in: outermost first. Only the - // propagators inside resonant_prop_mask have a row, and child_prop_mask - // names them by prop_index, so the two orders have to be related - // explicitly -- counting set mask bits instead lands on the wrong row as - // soon as a propagator in between is resonant without being a child. + // Row of each written resonance (outermost first), indexed by prop_index, as + // child_prop_mask refers to propagators by prop_index. std::vector res_index_of_prop(prop_count, -1); for (std::size_t prop_index = prop_count, next_res_index = 0; prop_index-- > 0;) { if (resonant_prop_mask & (1 << prop_index)) { @@ -724,8 +700,7 @@ void LHECompleter::complete_event_data( )) { --prop_index; if (resonant_prop_mask & (1 << prop_index)) { - // Descend through the propagators this event left off shell: they - // have no row of their own, so their children attach here. + // descend through propagators not written for this event int pending_prop_mask = propagator.child_prop_mask; while (pending_prop_mask != 0) { int child_prop_index = 0; @@ -851,8 +826,7 @@ void madspace::from_json( .flavor_count = j.at(7).get(), .diagram_count = j.at(8).get(), .helicity_count = j.at(9).get(), - // Gridpacks written before decays were supported carry no entry here - // and are always collisions. + // older files have no entry here and are collisions .incoming_count = j.size() > 10 ? j.at(10).get() : 2, }; } diff --git a/madspace/src/driver/madnis_training.cpp b/madspace/src/driver/madnis_training.cpp index c577aa149f..8f458f14ad 100644 --- a/madspace/src/driver/madnis_training.cpp +++ b/madspace/src/driver/madnis_training.cpp @@ -1113,8 +1113,8 @@ void MultiMadnisTraining::print_progress_update( ); _pretty_box_upper.print_update(); } else { - // Weight each subprocess's contribution to the global progress bar by its - // own batch count, since subprocesses no longer share a single batch count. + // weight each subprocess's share of the global progress bar by its batch + // count std::size_t total_batches = 0; std::size_t batches_before = 0; for (std::size_t i = 0; i < subproc_count; ++i) { diff --git a/madspace/src/driver/systematics.cpp b/madspace/src/driver/systematics.cpp index 8e3bf523dc..244e840a05 100644 --- a/madspace/src/driver/systematics.cpp +++ b/madspace/src/driver/systematics.cpp @@ -748,13 +748,10 @@ void SystematicsCalculator::compute( // per-event inputs struct EventInput { - // the scales of the MLM clustering vertices whose alpha_s is evaluated - // at their own scale rather than at mu_R (empty if none) + // scales of the MLM clustering vertices (empty if none) std::vector vertex_scales; - // the MLM pdf reweighting ladder of each beam, as madevent books it - // for its systematics (pdfrwt): the beam parton at x_beam and its - // leading scale, then for every step the line's flavour, its momentum - // fraction before the step and the scale the step takes it to + // MLM pdf ladder of each beam (madevent's pdfrwt): the beam parton + // at its x and leading scale, then one entry per step struct LadderEntry { double x, q; me_int_t slot; @@ -978,13 +975,11 @@ void SystematicsCalculator::compute( } } - // MLM pdf reweighting ladder, as madevent's systematics.py rebuilds it - // from the pdfrwt entries (q_0..q_N-1, x_0..x_N-1) of each beam: + // MLM pdf ladder as in madevent's systematics.py, per beam: // f(x_N-1, k S) prod_i ladder_events; for (std::size_t i = 0; use_pdf && i < count; ++i) { if (inputs[i].has_ladder) { @@ -1063,7 +1058,7 @@ void SystematicsCalculator::compute( } } - // alpha_s part: the distinct (alpha_s grid, mur, dyn) combinations, one + // alpha_s part: the distinct (alpha_s grid, mur, dyn, alps) combinations, one // batched alpha_s call each; the matrix element is re-evaluated for the // events of the mixed-order subprocesses std::vector keys; @@ -1151,14 +1146,10 @@ void SystematicsCalculator::compute( } } } - // MLM vertices, as madevent's systematics.py: the weight is - // alpha_s(mu_R)^(n - n_v) prod_v alpha_s(q_v), and a variation takes the - // n - n_v powers at k mu_R but every vertex at alpsfact q_v. The ratio - // above moved all n powers to k mu_R, so the vertices are taken back - // out of it and put in at their own scales: + // MLM vertices: the weight is alpha_s(mu_R)^(n - n_v) prod_v alpha_s(q_v), + // with vertices varied at alpsfact q_v. The ratio above moved all n + // powers to k mu_R, so the vertex factor is // prod_v alpha'(alps q_v) / alpha(q_v) * (alpha(mu_R) / alpha'(k mu_R))^n_v - // which holds for the matrix element re-evaluation too, since that - // ratio is the same (alpha'(k mu_R) / alpha(mu_R))^n. if (has_vertices) { std::vector vq; for (auto& in : inputs) { diff --git a/madspace/src/driver/thread_pool.cpp b/madspace/src/driver/thread_pool.cpp index f8a6356513..71670b750d 100644 --- a/madspace/src/driver/thread_pool.cpp +++ b/madspace/src/driver/thread_pool.cpp @@ -36,8 +36,6 @@ void ThreadPool::set_thread_count(int new_count) { } void ThreadPool::submit(JobFunc job) { - // Spin until there is space in the queue. The queue should be sufficiently large - // such that this never happens. std::unique_lock lock(_mutex); _job_queue.push_back(job); if (!_done_queue.empty()) { @@ -150,10 +148,8 @@ void ThreadPool::thread_loop(std::size_t index) { try { result = job(); } catch (...) { - // A job must never let an exception escape: it would unwind out of - // the thread function and terminate the process. Hand the first one - // to whoever is waiting, and drop the jobs still queued -- the - // waiter is about to unwind and they capture its locals. + // Keep the first exception for the waiter and drop the queued jobs, + // which capture the waiter's locals. lock.lock(); if (!_exception) { _exception = std::current_exception(); diff --git a/madspace/src/driver/vegas_optimizer.cpp b/madspace/src/driver/vegas_optimizer.cpp index 37c51948e4..ed98a1cf78 100644 --- a/madspace/src/driver/vegas_optimizer.cpp +++ b/madspace/src/driver/vegas_optimizer.cpp @@ -49,22 +49,19 @@ void VegasGridOptimizer::optimize() { } } - // apply smoothing + // smooth with a 3-point average (2-point at the edges) double prev_value = bin_values[0]; double current_value = bin_values[1]; double sum = 0.; - // bin_values[0] = (7. * prev_value + current_value) / 8.; bin_values[0] = (prev_value + current_value) / 2.; for (std::size_t i_bin = 1; i_bin < n_bins - 1; ++i_bin) { double next_value = bin_values[i_bin + 1]; - // double new_value = (prev_value + 6. * current_value + next_value) / 8.; double new_value = (prev_value + current_value + next_value) / 3.; bin_values[i_bin] = new_value; sum += new_value; prev_value = current_value; current_value = next_value; } - // bin_values[n_bins - 1] = (prev_value + 7. * current_value) / 8.; bin_values[n_bins - 1] = (prev_value + current_value) / 2.; // normalize and apply damping diff --git a/madspace/src/gpu/device.cu b/madspace/src/gpu/device.cu index 745e22f611..83823ff72d 100644 --- a/madspace/src/gpu/device.cu +++ b/madspace/src/gpu/device.cu @@ -276,7 +276,6 @@ std::vector> MemPool::total_sizes() const { std::pair AsyncGpuDevice::allocate(std::size_t size, AllocHint hint) const { - // if (_mem_pool && hint != AllocHint::normal && size <= 4 * 1024 * 1024) { if (_mem_pool && hint != AllocHint::normal && hint != AllocHint::output && size <= 4 * 1024 * 1024) { return _mem_pool->allocate( diff --git a/madspace/src/gpu/random.cuh b/madspace/src/gpu/random.cuh index 3764357717..a514c38b82 100644 --- a/madspace/src/gpu/random.cuh +++ b/madspace/src/gpu/random.cuh @@ -15,8 +15,8 @@ namespace madspace { namespace gpu { -// One MIXMAX substream per DerivedSeed stream index; the bank covers all of them so -// every RNG-consuming instruction draws from its own non-overlapping substream. +// Number of MIXMAX engines in the bank; engine i is the substream selected by index i +// in the low SEED_STREAM_BITS of the seed. constexpr std::size_t RNG_ENGINE_COUNT = 1 << 12; // The low SEED_STREAM_BITS of the effective 128-bit seed are the per-engine stream diff --git a/madspace/src/gpu/runtime.cu b/madspace/src/gpu/runtime.cu index 1b2624b72f..ab5bd63bf5 100644 --- a/madspace/src/gpu/runtime.cu +++ b/madspace/src/gpu/runtime.cu @@ -1440,9 +1440,8 @@ void op_histogram( thrust::device_pointer_cast(static_cast(square_weights_tmp.data())); auto square_values_ptr = thrust::device_pointer_cast(static_cast(square_values.data())); - // one sort of the bin indices, carrying the weights AND the squared weights along: - // sorting the indices again for the squared weights (as before) left these in the - // event order, so the squared weights of the wrong events were summed in each bin + // one sort of the bin indices carries weights and squared weights along, so both + // stay matched to the same events within each bin thrust::sort_by_key( policy, indices_ptr, @@ -1571,12 +1570,6 @@ GpuRuntime::GpuRuntime(const Function& function_arg, ContextPtr context) : auto& gpu_device = *static_cast(_context->device()); gpu_device.activate(); - // cudaMemPool_t pool; - // check_error(cudaDeviceGetMemPool(&pool, 0)); - // uint64_t thresh = UINT64_MAX; - // check_error(cudaMemPoolSetAttribute(pool, cudaMemPoolAttrReleaseThreshold, - // &thresh)); - Function function = sort_breadth_first(function_arg); _locals_init.resize(function.locals().size()); diff --git a/madspace/src/kernels/chili.hpp b/madspace/src/kernels/chili.hpp index 94a30d9826..acec1eba84 100644 --- a/madspace/src/kernels/chili.hpp +++ b/madspace/src/kernels/chili.hpp @@ -21,7 +21,7 @@ KERNELSPEC void kernel_chili_forward( FVal det_tmp(1.); FVal _e_cm(e_cm); auto e_cm2 = _e_cm * _e_cm; - auto pt_max = _e_cm / 2; // generally too large, that's why we get x1x2 > 1 and NaNs + auto pt_max = _e_cm / 2; // loose bound; points with x1 or x2 >= 1 get zero weight auto pt2_max = pt_max * pt_max; auto n_out = m_out.size(); for (std::size_t i = 0; i < n_out - 1; ++i) { @@ -185,7 +185,7 @@ KERNELSPEC void kernel_chili_inverse( auto det_pt = where(has_pt_cut, det_pt_withcut, det_pt_nocut); // ---- invert phi - // forward: phi = 2π r_phi + atan2(py_sum, px_sum) + // forward: phi = 2 pi r_phi + atan2(py_sum, px_sum) if (i == 0) { auto phi = atan2(py, px); phi = where(phi < 0, phi + 2. * PI, phi); @@ -202,7 +202,6 @@ KERNELSPEC void kernel_chili_inverse( } // ---- compute y from momentum (same rapidity used forward) - // wrong if pt is wrong, so fix pt first auto y = 0.5 * log((E + pz) / (E - pz)); auto mt2 = pt2 + m_out_i * m_out_i; auto mt = sqrt(mt2); diff --git a/madspace/src/kernels/cuts.hpp b/madspace/src/kernels/cuts.hpp index e2740959ba..ddfdc7d747 100644 --- a/madspace/src/kernels/cuts.hpp +++ b/madspace/src/kernels/cuts.hpp @@ -29,12 +29,8 @@ kernel_cut_one(FIn obs, FSIn min, FSIn max, FOut w) { w = where((obs < min) | (obs > max), FVal(0.), 1.); } -// The window [min, max] is one pair of scalars applied to every observable of the -// vector, as the instruction set declares them (cut_all/cut_any: min, max [float]). -// They must be rank 0 (here FSIn, as they are declared single): as rank-1 -// inputs the runtime built a 2-dimensional view of these 1-dimensional tensors, -// from shape/stride entries that were never set, and min[i] read out of bounds -// (a memory fault on HIP, silently min[0] elsewhere). +// The window [min, max] is a single pair of scalars applied to every observable of +// the vector. template KERNELSPEC void kernel_cut_all(FIn obs, FSIn min, FSIn max, FOut w) { diff --git a/madspace/src/kernels/kinematics.hpp b/madspace/src/kernels/kinematics.hpp index 36ce7b5458..6a87a0eb96 100644 --- a/madspace/src/kernels/kinematics.hpp +++ b/madspace/src/kernels/kinematics.hpp @@ -102,8 +102,7 @@ t_inv_min_max(FVal s, FVal ma_2, FVal mb_2, FVal m1_2, FVal m2_2) auto t_min_tmp = -max(y2, y1); auto t_max_tmp = -min(y1, y2); auto t_min = max(t_min_tmp, 0.); - // an empty range gets a width of a few ulps rather than an absolute EPS, - // which is below one ulp of t for |t| > ~1e4 and left max == min + // an empty range gets a width of a few ulps, since an absolute EPS can be below one ulp of t auto t_max = where( t_max_tmp > t_min, t_max_tmp, t_min + max(FVal(EPS), 1e-14 * fabs(t_min)) ); @@ -196,22 +195,6 @@ KERNELSPEC FVal esquare(FourMom p) { return p[1] * p[1] + p[2] * p[2] + p[3] * p[3]; } -// Rotation R(q) that takes the z axis onto the direction of q, used to set up -// the scattering frame around an incoming momentum. For q in the upper -// hemisphere it is the minimal rotation about z x q, -// -// [ 1 - a^2 h -a b h a ] -// R_+ = [ -a b h 1 - b^2 h b ], (a, b, c) = q / |q|, h = 1 / (1 + c), -// [ -a -b c ] -// -// and in the lower one F R_+(F q) with F = diag(-1, 1, -1), the rotation by pi -// about y. Both are smooth across q parallel to the z axis, where they reduce -// to the identity and to F. The frame therefore moves only at rounding level -// when q does, so forward and inverse agree on the azimuth even when they -// build q from differently rounded momenta. (The previous construction switched -// between an azimuth set by atan2(q_y, q_x) and a fixed one on the exact test -// q_t == 0, and turned the azimuth by an arbitrary angle, often pi/2, whenever -// the two sides rounded q_t differently.) template KERNELSPEC FVal ldot(FourMom p1, FourMom p2) { return p1[0] * p2[0] - p1[1] * p2[1] - p1[2] * p2[2] - p1[3] * p2[3]; @@ -222,6 +205,16 @@ KERNELSPEC FVal edot(FourMom p1, FourMom p2) { return p1[1] * p2[1] + p1[2] * p2[2] + p1[3] * p2[3]; } +// Rotation R(q) that takes the z axis onto the direction of q. For q in the upper +// hemisphere it is the minimal rotation about z x q, +// +// [ 1 - a^2 h -a b h a ] +// R_+ = [ -a b h 1 - b^2 h b ], (a, b, c) = q / |q|, h = 1 / (1 + c), +// [ -a -b c ] +// +// and in the lower one F R_+(F q) with F = diag(-1, 1, -1), the rotation by pi +// about y. Both are smooth across q parallel to the z axis, so the frame moves +// only at rounding level when q does and forward and inverse agree on the azimuth. template KERNELSPEC FourMom rotate(FourMom p, FourMom q) { auto qq = sqrt(max(esquare(q), FVal(1e-300))); @@ -259,16 +252,11 @@ KERNELSPEC FourMom rotate_inverse(FourMom p, FourMom q) { // along q_z and x_hat along the part of q_x perpendicular to it. Shared by // rotate_two_ref and its inverse so that both build the identical frame. // -// Both directions are normalised by their own length first, so the frame -// does not depend on the scale of q_x: a soft q_x (a soft recoil momentum in -// the 2->3 block) used to hit an absolute floor on |q_x perp| that left x_hat -// shorter than 1, the rotation shrank p, and the daughters came out far off -// shell. The perpendicular part gets a second Gram-Schmidt pass: when q_x is -// close to z_hat the first one leaves a component along z_hat of relative -// size eps / sin(angle), the frame is not orthonormal, and the rotation -// changes |p|. When q_x has no usable perpendicular part (parallel to q_z to -// ~1e-14, or zero) the azimuth is undefined, and a fixed axis perpendicular -// to z_hat is taken instead. +// Both vectors are normalised by their own length, so the frame does not depend +// on their scale. The perpendicular part of q_x gets a second Gram-Schmidt pass to +// keep the frame orthonormal when q_x is close to z_hat. If q_x has no usable +// perpendicular part (parallel to q_z to ~1e-14, or zero) a fixed axis +// perpendicular to z_hat is used. template KERNELSPEC Triplet, FourMom, FourMom> two_ref_frame(FourMom q_z, FourMom q_x) { @@ -352,8 +340,7 @@ rotate_two_ref_inverse(FourMom p, FourMom q_z, FourMom q_x) { template KERNELSPEC FourMom boost(FourMom k, FourMom p_boost, FVal sign) { - // Perform the boost - // This is in fact a numerically more stable implementation than often used + // numerically more stable than the commonly used form auto p2_boost = lsquare(p_boost); auto rsq = sqrt(max(EPS2, p2_boost)); auto k_dot_p = k[1] * p_boost[1] + k[2] * p_boost[2] + k[3] * p_boost[3]; @@ -368,22 +355,13 @@ template KERNELSPEC FourMom boost_light_cone( FourMom k, FVal k_mass2, FourMom p_boost, FVal p_mass2, FVal sign ) { - // Same boost as `boost`, for a momentum k of known mass^2 k_mass2, written - // in light-cone components along the boost axis n: - // k_+ -> e^{+y} k_+, k_- -> e^{-y} k_-, k_perp unchanged, - // e^{y} = (p0 + |p|) / m. - // In `boost` a momentum thrown against a boost with gamma >> 1 comes out of - // the cancellation of terms of order gamma E and loses a factor gamma^2 of - // its relative precision, which leaves light-like momenta off shell by up - // to a few 1e-6 of their energy in the long color-ordered chains. Here the - // smaller of k_+, k_- is taken from the mass shell, - // k_+ k_- = k_mass2 + k_perp^2, both boosted components are positive - // products, and the result is accurate to a few ulps of its own energy, - // including its mass. p_mass2 is the mass^2 of p_boost, as the caller has - // already computed it, so that the boost is the one of the rest frame the - // caller built k in. - // |p| only sets the direction n; an absolute floor here (EPS2) left n - // shorter than 1 for a system almost at rest, |p| < 1e-12 + // Same boost as `boost` for a momentum k of known mass^2 k_mass2, in light-cone + // components along the boost axis n: k_+ -> e^{+y} k_+, k_- -> e^{-y} k_-, + // k_perp unchanged, e^y = (p0 + |p|) / m. The smaller of k_+, k_- is taken from + // the mass shell, k_+ k_- = k_mass2 + k_perp^2, so both boosted components are + // products of positive numbers and k stays on shell to a few ulps even against + // a boost with gamma >> 1. p_mass2 is the mass^2 of p_boost. + // |p| only sets the direction n, hence a tiny floor. auto p_mag = sqrt(max(esquare(p_boost), FVal(1e-300))); auto m_p = sqrt(max(p_mass2, EPS2)); auto e_plus = (p_boost[0] + p_mag) / m_p; @@ -415,12 +393,10 @@ KERNELSPEC FourMom boost_light_cone( } // Lab momenta of a two-body system p_tot -> k1 + k2 given back to back in its -// rest frame, with k1 + k2 = p_tot by construction. The softer of the two is -// boosted with boost_light_cone on its own mass shell and the harder one is -// p_tot minus it. The harder one carries at least half the energy, so it keeps -// the relative precision of p_tot; taking the softer one as the difference -// instead (or boosting it with `boost`) leaves it off shell when it is thrown -// backwards against a fast p_tot. +// rest frame, with k1 + k2 = p_tot by construction. The softer one is boosted +// with boost_light_cone on its own mass shell and the harder one is p_tot minus +// it, so that the harder one (at least half the energy) keeps the precision of +// p_tot and the softer one stays on shell. template KERNELSPEC Pair, FourMom> boost_two_body( FourMom k1_rest, @@ -450,19 +426,11 @@ KERNELSPEC void boost_beam( FVal sign, FOut p_out ) { - // Boost along z with rapidity y = log(x1 / x2) / 2. The first two momenta - // are the incoming ones. - // - // Written as E cosh y + p_z sinh y, a momentum moving against the boost - // comes out of the cancellation of terms of order e^{|y|} E and loses a - // factor e^{2|y|} of relative precision; light-like momenta ended up off - // shell by up to a few 1e-5 of their energy in the lab frame. For those - // momenta (lab energy below the partonic one) the boost is done in - // light-cone components, q_+ = E + p_z -> e^{+y} q_+, - // q_- = E - p_z -> e^{-y} q_-, with the smaller of q_+, q_- taken from the - // mass shell, q_+ q_- = m^2 + p_T^2, and the known mass of the particle (a - // negative entry means unknown, and the mass is read off the momentum). - // All other momenta keep the linear boost, which is accurate for them. + // Boost along z with rapidity y = log(x1 / x2) / 2 (inverse for sign < 0); the + // first two momenta are the incoming ones. Momenta that are on shell and lose + // energy in the boost are boosted in light-cone components (as in + // boost_light_cone) to avoid cancellation; masses[i] < 0 means the mass is + // read off the momentum. All others use the linear boost. auto exp_rap = sqrt(x1 / x2); auto exp_rap_inv = sqrt(x2 / x1); auto e_plus = where(sign > 0., exp_rap, exp_rap_inv); @@ -488,11 +456,7 @@ KERNELSPEC void boost_beam( where(q3 < 0., q_minus_direct, trans2 / max(q_plus_direct, FVal(EPS2))); auto p_plus = e_plus * q_plus; auto p_minus = e_minus * q_minus; - // Only for a momentum that is on its mass shell to rounding. The - // projection moves it by its mass-shell error times e^{|y|}; for one - // that is not (a degenerate point, or partonic momenta that carry - // larger errors from elsewhere) that would be more than the precision - // gained, and it keeps the linear boost. + // light-cone boost only for momenta on shell to rounding, else keep the linear one auto on_shell = fabs(q_plus_direct * q_minus_direct - trans2) <= 1e-13 * q0 * q0; auto light_cone = @@ -503,12 +467,9 @@ KERNELSPEC void boost_beam( p_out_i[3] = where(light_cone, 0.5 * (p_plus - p_minus), lin_3); } - // The light-cone boost also moves a momentum onto its mass shell, by its - // (tiny) partonic mass-shell error amplified by e^{|y|}. So that momentum - // stays conserved exactly as with the linear boost, these shifts are handed - // to the outgoing momentum with the largest energy: it is set to its linear - // boost minus the shifts of all others. It carries at least 1/n of the - // energy, so this costs it no relative precision. + // The light-cone boost shifts momenta onto their mass shell. To keep momentum + // conservation exact, the outgoing particle with the largest energy is set to + // its linear boost minus the shifts of all the others. if (q.size() > 3) { auto best_e = FVal(p_out[2][0]); auto best_i = IVal(2); @@ -816,11 +777,9 @@ KERNELSPEC Pair, FVal> t_cut_bounds_etmin( return {ratio * (base - rootsq) - m1_2, ratio * (base + rootsq) - m1_2}; } -// Clamp the kinematic |t| range against the ETmin cut interval. Both kernels -// return the absolute (positive) t invariant. The ETmin bound is a sampling -// optimization, not the cut enforcement: the integrator applies the real cut on -// the final momenta (cf. AmpliCol's explicit ET-check on the generated -// solutions). +// Clamp the kinematic |t| range against the ETmin cut interval. Returns the +// bounds on the absolute (positive) t invariant. The ETmin bound only improves sampling; the +// real cut is applied on the final momenta. template KERNELSPEC void kernel_t_inv_min_max_cut( FIn pa, @@ -849,10 +808,8 @@ KERNELSPEC void kernel_t_inv_min_max_cut( auto cut = t_cut_bounds_etmin(p_tot, load_mom(pa), m1_2, m2_2, etmin_1, etmin_2); - // If the incoming particle pa is not along the beam axis, the ETmin cut is - // is too tight. In that case, we just use the kinematic bounds. - // auto tmn_c = max(tmn, cut.first); - // auto tmx_c = min(tmx, cut.second); + // The ETmin bound is too tight if pa is not along the beam axis; then only the + // kinematic bounds are used. auto proj_pt2 = pa[1] * pa[1] + pa[2] * pa[2]; auto proj_along_z = proj_pt2 <= (pa[0] * pa[0]) * 1e-9; auto tmn_c = where(proj_along_z, max(tmn, cut.first), tmn); @@ -947,7 +904,7 @@ KERNELSPEC void kernel_t_rapidity_clamp( t_max_out = where(ok, hi, FVal(t_max)); } -// Narrows |t1| of the double-t central block (see t2_rapidity_clamp_doublet) to +// Narrows |t1| of the double-t central block (see kernel_t2_rapidity_clamp_doublet) to // the values that leave some |t2| for the rapidity bounds. At fixed |t1| every // bound on |t2| is linear in |t1|: // p1 (bound y_max1): (m1^2 + |t1|) e^{2 lo1} - m1^2 <= |t2| @@ -1045,14 +1002,14 @@ KERNELSPEC void kernel_t1_rapidity_clamp_doublet( } // Narrows |t2| of the double-t central block pa pb -> p1 p2 (pa, pb the massless -// beams, |t1| = -(pa - p1)^2, |t2| = -(pb - p1)^2, see double_t_scattering) +// beams, |t1| = -(pa - p1)^2, |t2| = -(pb - p1)^2, see kernel_double_t_scattering) // given |t1| and bounds on the absolute lab rapidities of p1 (y_max1) and p2 // (y_max2); negative means no bound. Here pa.p1 = (m1^2 + |t1|) / 2 and // pb.p1 = (m1^2 + |t2|) / 2, and for p2 = pa + pb - p1, pa.p2 = s/2 - pa.p1 and // pb.p2 = s/2 - pb.p1, so both rapidities along pa, log(pb.q / pa.q) / 2, are // monotonic in |t2| at fixed |t1|. The lab rapidity is that plus y_boost = // beam_sign * log(x1 / x2) / 2. An empty result returns the input range, as in -// t_rapidity_clamp. +// kernel_t_rapidity_clamp. template KERNELSPEC void kernel_t2_rapidity_clamp_doublet( FIn t2_min, @@ -1083,7 +1040,7 @@ KERNELSPEC void kernel_t2_rapidity_clamp_doublet( auto on1 = rapidity_bound_on(y_max1), on2 = rapidity_bound_on(y_max2); // bounds on w = pb.p1, turned into |t2| = 2 w - m1^2 before they meet the - // input range (see t_rapidity_clamp) + // input range (see kernel_t_rapidity_clamp) FVal lo = t2_min, hi = t2_max; // p1: w / a1 = exp(2 y_a), increasing in w auto w_lo1 = a1 * bounded_exp(2. * (-y_max1 - y_boost)); @@ -1131,8 +1088,6 @@ KERNELSPEC void kernel_t_inv_value_and_min_max_cut( // particle 1 = recoil (m1, etmin_1), particle 2 = peeled (m2, etmin_2) auto cut = t_cut_bounds_etmin(p_tot, load_mom(pa), m1_2, m2_2, etmin_1, etmin_2); - // auto tmn_c = max(tmn, cut.first); - // auto tmx_c = min(tmx, cut.second); auto proj_pt2 = pa[1] * pa[1] + pa[2] * pa[2]; auto proj_along_z = proj_pt2 <= (pa[0] * pa[0]) * 1e-9; auto tmn_c = where(proj_along_z, max(tmn, cut.first), tmn); @@ -1309,10 +1264,9 @@ kernel_momenta_to_x1x2(FIn p_ext, FSIn e_cm, FOut x1, FOut KERNELSPEC void kernel_boost_to_frame(FIn p_ext, ISIn frame_mask, FOut p_out) { @@ -1330,10 +1284,7 @@ kernel_boost_to_frame(FIn p_ext, ISIn frame_mask, FOut p_out) selected_index = i; } if (selected_count == 0) { - // Nothing selected, so nothing defines a frame: pass the momenta - // through rather than boosting by a null vector, whose zero invariant - // mass boost() would floor at EPS2 and turn every energy into garbage. - // Same choice as the qq == 0 branch of the HELAS boostx madevent uses. + // no frame is defined: pass the momenta through (boosting by a null vector gives garbage) for (std::size_t i = 0; i < p_ext.size(); ++i) { store_mom(p_out[i], load_mom(p_ext[i])); } @@ -1341,19 +1292,9 @@ kernel_boost_to_frame(FIn p_ext, ISIn frame_mask, FOut p_out) } for (std::size_t i = 0; i < p_ext.size(); ++i) { auto p_i = boost(load_mom(p_ext[i]), p_boost, -1.); - // A single particle defining the frame must come out exactly at rest. - // boost() only gets there up to the rounding of the boost factor, - // leaving a residual three-momentum of a few 1e-14 whose direction is - // pure noise, and that is not harmless: vxxxxx branches on pp == 0 and - // for a massive vector at exactly zero three-momentum takes the frame z - // axis as quantisation axis; otherwise it builds the polarisation - // vectors from the momentum direction, i.e. from the rounding noise. So - // impose the defining property of the frame explicitly. The energy is - // left untouched: zeroing the three-momentum shifts the invariant mass - // by O(1e-28) relative, and HELAS takes the mass as a separate - // argument anyway. With two or more selected particles it is only their - // sum that is at rest and no single leg sits on the branch point, so - // this is needed for selected_count == 1 only. + // A single defining particle must be exactly at rest: the rounding residue of + // boost() would otherwise make polarisation vectors depend on noise. + // The energy is left untouched. if (selected_count == 1 && i == selected_index) { p_i[1] = 0.; p_i[2] = 0.; diff --git a/madspace/src/kernels/mlm.hpp b/madspace/src/kernels/mlm.hpp index 9e50a6c31f..4f03737b66 100644 --- a/madspace/src/kernels/mlm.hpp +++ b/madspace/src/kernels/mlm.hpp @@ -36,17 +36,15 @@ constexpr int LINE_GOODJET = 1; constexpr int MEASURE_FXFX = 0; constexpr int MEASURE_MADEVENT = 1; // How alpha_s is evaluated for a merged event. -constexpr int ALPHAS_NONE = 0; // one coupling at mu_R, as before +constexpr int ALPHAS_NONE = 0; // one coupling at mu_R constexpr int ALPHAS_PER_VERTEX = 1; // alphas(pt_i) at each vertex, as madevent constexpr int ALPHAS_GEOMETRIC = 2; // alphas at the geometric mean of the pt_i // trace_data bits above the 2-bit trace mode: the flavour of the mother. constexpr int TRACE_IS_JET_IN = 1 << 2; constexpr int TRACE_IS_COLORED_IN = 1 << 3; -// Bits 4..11 hold the mother's pdf flavour class plus one, so that zero means -// the line is not a parton and the pdf reweighting chain stops there. The -// classes themselves are assigned in mlm_clustering.cpp; the kernel only -// carries them, since which density they stand for is not known until the -// flavour has been sampled. +// Bits 4..11 hold the mother's pdf flavour class plus one; zero means the line +// is not a parton and the pdf reweighting chain stops. The classes are assigned +// in mlm_clustering.cpp and only carried here. constexpr int TRACE_FLAVOR_SHIFT = 4; constexpr int TRACE_FLAVOR_MASK = 0xFF; // More of the mother's flavour, for isjetvx and the iqjets demotion. @@ -55,11 +53,8 @@ constexpr int TRACE_MOTHER_IS_DAU1 = 1 << 13; constexpr int TRACE_MOTHER_IS_DAU2 = 1 << 14; constexpr int TRACE_ALL_COLORLESS = 1 << 15; // The clustering joins two bare external fermions through a flavour-diagonal -// boson (g, a, Z, h), so it only exists when the two carry the same flavour. -// One state machine serves every flavour of a merged subprocess, and the union -// of its diagrams has such a vertex for some flavours only: for q q~ > q' q~' -// the q-q' t-channel gluon of q q~ > q q~. The flavour is a per-event input, -// see flavor_index and leg_flavors. +// boson (g, a, Z, h), so it only exists when both carry the same flavour. The +// flavour is a per-event input, see flavor_index and leg_flavors. constexpr int TRACE_SAME_FLAVOR = 1 << 16; // jet_leg_mask carries is_jet per leg in the low half and is_octet in the high // half; n_ext_max is 12, so one word holds both. @@ -88,10 +83,8 @@ KERNELSPEC FVal dj_clus( FVal jet_radius ) { if (!hadronic) { - // Durham e+e- measure: 2*min(E1^2, E2^2)*(1 - cos_theta), evaluated as - // 4*min(E1^2, E2^2)*sin^2(theta/2) with theta = atan2(|p1 x p2|, - // p1.p2). For a nearly collinear pair 1 - cos_theta is the difference - // of two numbers close to one and loses most of its digits. + // Durham measure 2*min(E1^2, E2^2)*(1 - cos_theta), evaluated as + // 4*min(E1^2, E2^2)*sin^2(theta/2) to avoid cancellation for collinear pairs. auto p1a = sqrt(esquare(p1)); auto p2a = sqrt(esquare(p2)); if (p1a * p2a == 0.0) { @@ -108,7 +101,7 @@ KERNELSPEC FVal dj_clus( return 4.0 * min(p1[0] * p1[0], p2[0] * p2[0]) * sin_half * sin_half; } - // hadronic: massless+massive pair clusters to the lighter parton's mT^2 + // hadronic: massless+massive pair scores the massless parton's mT^2 bool massive1 = (mass1 > 0.0); bool massive2 = (mass2 > 0.0); if (!massive1 && massive2) { @@ -118,14 +111,10 @@ KERNELSPEC FVal dj_clus( return djb_clus(p2, true) * ONE_PLUS_TINY; } - // both massless or both massive: generalised kt measure in (eta, phi), + // both massless or both massive: generalised kt measure // min(pt1^2, pt2^2) * 2 * (cosh(deta) - cos(dphi)) / D^2 as in cluster.f, - // evaluated as 4 * (sinh^2(deta/2) + sin^2(dphi/2)), with dphi from - // atan2(cross, dot) and eta = asinh(pz/pt). For a nearly collinear pair - // cosh(deta) and cos(dphi) are both close to one, and their difference - // lost up to eight digits, so that scalar and SIMD math (and the GPU) gave - // different scales; log((|p| + pz)/(|p| - pz)) also cancels for a forward - // parton. + // evaluated as 4 * (sinh^2(deta/2) + sin^2(dphi/2)) to avoid cancellation + // for collinear pairs; eta = asinh(pz/pt), dphi from atan2(cross, dot). auto pt1_sq = p1[1] * p1[1] + p1[2] * p1[2]; auto pt2_sq = p2[1] * p2[1] + p2[2] * p2[2]; if (pt1_sq == 0.0 || pt2_sq == 0.0) { @@ -141,9 +130,7 @@ KERNELSPEC FVal dj_clus( min(pt1_sq, pt2_sq) * 4.0 * (sinh_half * sinh_half + sin_half * sin_half) / (jet_radius * jet_radius); - // The Fortran zeroes both negative and NaN results here ("prevent - // numerical inaccuracies"); max() alone would let a NaN from extreme - // inputs through. + // The Fortran zeroes negative and NaN results; max() alone would pass a NaN. if (!(result > 0.0)) { return 0.0; } @@ -154,12 +141,12 @@ KERNELSPEC FVal dj_clus( // mass1/mass2 are the tracked clustering masses for momentum1/momentum2. // // Parameters replacing Fortran globals / derived quantities: -// is_initial : momentum2 is a beam particle (Fortran: j<=2 in cluster_one_step) +// is_initial : momentum1 is a beam particle (state machine particle1 < 2) // hadronic : hadronic collider (Fortran: lpp[] from run.inc) -// jet_radius : jet-radius parameter (Fortran: common /to_dj/D) +// jet_radius : jet-radius parameter (Fortran: common /to_dj/D) // massive_in/out1/out2 replace the Fortran get_clustering_type cl[0:2] bit-array // resonant : replaces the iBWlist lookup in Fortran cluster_scale -// mass_in/width_in: carried for future use; not consumed in the scale formula +// measure : MEASURE_FXFX or MEASURE_MADEVENT // // based on cluster_scale from Template/NLO/SubProcesses/cluster.f template @@ -182,11 +169,8 @@ KERNELSPEC FVal compute_scale( // scale = mT of the final-state parton // small penalty when it goes against the beam if (measure == MEASURE_MADEVENT) { - // LO cluster.f multiplies pt2ij, the squared measure, by 1 + 1e-6, - // which is exactly the factor DJ gives a massless-massive - // final-state pair: the two tie, and madevent keeps the - // initial-state candidate it evaluated first. Taking the square - // root of the same product keeps the tie bit-for-bit here. + // LO cluster.f scales the squared measure by 1 + 1e-6; the same + // product is square-rooted here so ties with DJ match bit-for-bit. auto djb = djb_clus(momentum2, hadronic); if ((momentum1[3] < 0.0) != (momentum2[3] < 0.0)) { djb = djb * ONE_PLUS_TINY; @@ -203,14 +187,9 @@ KERNELSPEC FVal compute_scale( return sqrt(max(lsquare(momentum_sum), 0.0)); } if (measure == MEASURE_MADEVENT) { - // DJ of Template/LO/Source/kin_functions.f, which madevent's LO - // cluster.f takes for every final-state pair: dj_clus already scores a - // massless-massive pair by the massless one's transverse mass (times - // 1 + 1e-6) and anything else by the kt measure with the larger mass - // squared added. The branches below are the NLO cluster_scale's, which - // score q* > q W and g* > g h by sqrt(|p_j.(p_i+p_j)|)/2 instead - often - // below the jet's pt, so the jet pairs with the boson where madevent - // takes it into a beam. + // DJ of Template/LO/Source/kin_functions.f, used by madevent's LO + // cluster.f for every final-state pair. The branches below are the NLO + // cluster_scale's, which score q* > q W and g* > g h differently. return sqrt(dj_clus(momentum1, momentum2, mass1, mass2, hadronic, jet_radius)); } if (!massive_in && massive_out1 && !massive_out2) { @@ -225,28 +204,15 @@ KERNELSPEC FVal compute_scale( return sqrt(dj_clus(momentum1, momentum2, mass1, mass2, hadronic, jet_radius)); } -// Two candidate clusterings whose measures agree to this relative precision are -// taken to be equal, and settled by order rather than by rounding. +// Candidate measures agreeing to this relative precision are treated as ties. constexpr double TIE_TOLERANCE = 1e-12; // Whether a candidate clustering takes over from the best one found so far. // // A resonant candidate beats a non-resonant one, then the smaller measure wins. -// Measures that are equal in exact arithmetic are common: with two massless -// partons left in the final state they balance in transverse momentum, so -// clustering either one into the beam it travels along scores the same mT. -// Comparing the rounded values then hands the step to whichever came out an -// ulp lower, which a rotation about the beam axis can change, and which beam -// takes the parton decides where that beam's line ends and so mu_F. -// -// Settle such ties the way madevent's cluster.f does in exact arithmetic: it -// keeps the first of two equal candidates, looping over the later leg on the -// outside and its partner, beams first, on the inside. cluster.f keeps the -// surviving legs in their original order with the mother in the earlier -// slot, as the slots here are kept, so its loop order is the order of -// (particle2, particle1). That also covers the tie DJ makes between a beam -// clustering and a massless-massive final-state pair on the same parton: -// the beam comes first. +// Ties (equal in exact arithmetic, e.g. two balanced massless partons clustered +// into either beam) are settled as in cluster.f: the first candidate in loop +// order, i.e. by (particle2, particle1), is kept. template KERNELSPEC bool candidate_wins( FVal scale, @@ -311,10 +277,9 @@ KERNELSPEC void update_momenta( -(momenta[i_keep][3] + momenta[j_other][3]), }; // cluster.f skips the boost at the last clustering ("Don't boost if boost - // vector too lightlike or last vertex", nleft > 4), so the frame the - // 2 -> 1 root is measured in is the one left by the previous step. + // vector too lightlike or last vertex"), see allow_boost. if (allow_boost && lsquare(com_boost_vector) > 100.0) { - // boost j_keep to COM frame to define the rotation axis, then apply to all + // boost i_keep to COM frame to define the rotation axis, then apply to all // alive particles auto jkeep_cm = boost(momenta[i_keep], com_boost_vector, 1.0); for (int j = 0; j < n_part; ++j) { @@ -392,14 +357,9 @@ KERNELSPEC int mlm_clustering_walk( int cluster_max = momenta.size() - 3; int n_part = momenta.size(); FourMom momenta_tmp[N_EXT_MAX]; - // The same clustering tracked in the lab frame, without the boosts and - // rotations update_momenta applies. Only the 2 -> 1 root needs it, and only - // when the last clustering was a final-state one: cluster.f then boosts and - // rotates the remaining object back before taking the root's transverse - // mass. It undoes only the most recent boost; this copy undoes all of them, - // which is the same thing whenever at most one boosting initial-state - // clustering came before. After an initial-state last clustering cluster.f - // takes the root in the current frame instead, see last_is_initial. + // The same clustering in the lab frame, without update_momenta's boosts and + // rotations. Used for the 2 -> 1 root after a final-state last clustering + // (cluster.f boosts back first); see last_is_initial. FourMom momenta_lab[N_EXT_MAX]; // Whether the last recorded clustering was an initial-state one. bool last_is_initial = false; @@ -416,11 +376,8 @@ KERNELSPEC int mlm_clustering_walk( // zcl in cluster.f, zero for a final-state one. The pdf reweighting walks // the beam's momentum fraction down the ladder with it. FVal cluster_z[N_EXT_MAX - 3]; - // sqrt(mT(d1) mT(d2)) of the last clustering if that one is a final-state - // clustering, zero otherwise. mt2last in cluster.f, kept unsquared like - // every other scale here: "If last clustering is FS, store also average - // transverse mass of the particles combined (for use if QCD vertex, e.g. - // tt~ or qq~)". + // sqrt(mT(d1) mT(d2)) of the last clustering if it is a final-state one, + // zero otherwise. mt2last in cluster.f, kept unsquared. FVal mt_last = 0.0; for (int i = 0; i < n_part; ++i) { @@ -444,8 +401,8 @@ KERNELSPEC int mlm_clustering_walk( int win_next_state = -1, win_data = 0, win_trace = TRACE_FIRST; bool win_resonant = false; FVal win_scale = SCALE_MAX * 10.; - // The best of the candidates the flavours of the event forbid, taken only - // when a state offers nothing else, so that the walk can always go on. + // Best candidate forbidden by the event flavours; used only if nothing else + // is left, so that the walk can always continue. int alt_next_state = -1, alt_data = 0, alt_trace = TRACE_FIRST; bool alt_resonant = false; FVal alt_scale = SCALE_MAX * 10.; @@ -496,20 +453,14 @@ KERNELSPEC int mlm_clustering_walk( clustering_measure ); - // An exactly collinear pair - which the boost and rotation applied after - // an initial-state clustering can produce - makes the measure inf or - // NaN. Map those onto a large finite value: they then lose to any - // well-defined clustering, but a clustering is still always chosen, so - // the walk cannot fall off the end of the state machine. + // A collinear pair can make the measure inf or NaN; map it to a large finite + // value so it loses but a clustering is still always chosen. if (!(scale < SCALE_MAX)) { scale = SCALE_MAX; } - // The MG5 fortran code extracted the resonance structure from the integration - // channel. This is not always possible in MG7, so prefer resonant configs - // over non-resonant ones. Ties go the way cluster.f's loop order takes - // them whatever order the state machine lists the candidates in, see - // candidate_wins. + // MG5 took the resonance structure from the integration channel, which is + // not always possible in MG7, so resonant candidates are preferred. bool forbidden = (trace_data & TRACE_SAME_FLAVOR) && leg_flavor[particle1] != 0 && leg_flavor[particle2] != 0 && leg_flavor[particle1] != leg_flavor[particle2]; @@ -546,28 +497,21 @@ KERNELSPEC int mlm_clustering_walk( if (is_last) { int p1_win = win_data & 0xFF; int p2_win = (win_data >> 8) & 0xFF; - // Read the daughter before update_momenta boosts it away. Only an - // initial-state clustering has one, matching the iwin < 3 guard in - // cluster.f. + // Read before update_momenta boosts it; only initial-state clusterings + // have one (iwin < 3 guard in cluster.f). cluster_mt[cluster_count] = p1_win < 2 ? sqrt(djb_clus(momenta_tmp[p2_win], hadronic)) : FVal(0.0); - // cluster.f sets mt2last when the clustering leaves only the two - // beams and one object (nleft <= 3) and was a final-state one - // (iwin > 2), from pcl as it stands then: after any earlier - // initial-state boosts, before this clustering merges the pair. + // mt2last of cluster.f: set when this final-state clustering leaves + // the two beams and one object, from the momenta before merging. if (cluster_count == cluster_max - 1 && p1_win >= 2) { mt_last = sqrt(sqrt( djb_clus(momenta_tmp[p1_win], hadronic) * djb_clus(momenta_tmp[p2_win], hadronic) )); } - // zclus() of Template/LO/Source/kin_functions.f: the ratio of the - // partonic invariants before and after the emission is taken back - // out of the beam, both measured against the other beam. Taken - // here, before update_momenta, because that is where cluster.f - // takes it; the boosts it applies preserve the dot products - // anyway. + // zclus() of Template/LO/Source/kin_functions.f: ratio of the partonic + // invariants after and before the emission is removed from the beam. cluster_z[cluster_count] = 0.0; if (p1_win < 2) { int other_beam = 1 - p1_win; @@ -580,17 +524,13 @@ KERNELSPEC int mlm_clustering_walk( } FVal s_prev = lsquare(sum_prev); FVal s_red = lsquare(sum_red); - // The Fortran gives up below 1 GeV^2 rather than dividing; - // a z of zero then fails the 0 < z < 1 test downstream and the - // momentum fraction is left alone. + // Like the Fortran, give up below 1 GeV^2; z = 0 is ignored downstream. if (s_red >= 1.0 && s_prev > 0.0) { cluster_z[cluster_count] = s_red / s_prev; } } - // The mother keeps slot p1_win. A final-state clustering merges the - // two daughters; an initial-state one takes the emission back out - // of the beam, as pcl(imo) = pcl(ida1) - pcl(ida2) does in - // cluster.f. + // The mother keeps slot p1_win; initial-state clusterings subtract + // the emission, as pcl(imo) = pcl(ida1) - pcl(ida2) in cluster.f. for (int k = 0; k < 4; ++k) { momenta_lab[p1_win][k] = p1_win < 2 ? momenta_lab[p1_win][k] - momenta_lab[p2_win][k] @@ -614,10 +554,7 @@ KERNELSPEC int mlm_clustering_walk( cluster_trace[cluster_count] = win_trace; cluster_scales[cluster_count] = win_scale; ++cluster_count; - // Reset the whole selection, not just the scale: leaving - // win_resonant set would stop any non-resonant candidate from ever - // winning the next step, and leaving win_next_state set would send - // the walk to a stale state if that happened. + // Reset the whole selection for the next step, not just the scale. win_next_state = -1; win_data = 0; win_trace = TRACE_FIRST; @@ -685,16 +622,8 @@ KERNELSPEC int mlm_clustering_walk( bool by_production = jet_scale_scheme == SCHEME_PRODUCTION; FVal ren_scale_val = 1.0; - // The generation-level merging cut. A clustering that emitted a jet has to - // be at or above xqcut, otherwise the matrix element is describing - // radiation the parton shower is meant to produce and the event is - // dropped. This mirrors the "Check xqcut for vertices with jet daughters - // only" block of Template/LO/SubProcesses/reweight.f: the test runs per - // clustering step, on each daughter that is still a bare final-state jet, - // and it does not ask whether the vertex was a QCD one. - // - // is_last_cluster is exactly the "still a bare external leg" bookkeeping - // the emission scheme already needs, so the two share it. + // Bitmask of slots that are still bare external legs (bits 2..n_part-1). + // Used for the emission scheme and the xqcut cut below. int is_last_cluster = 0b11111111'11111111'11111100; for (int i = 0; i < cluster_max; ++i) { FVal scale = cluster_scales[i]; @@ -743,9 +672,7 @@ KERNELSPEC int mlm_clustering_walk( step_daughter_legs[i][2] = rep1[particle2]; step_daughter_legs[i][3] = rep2[particle2]; - // Carry the parton line into the mother, which occupies slot - // particle1. Only needed for the production scheme, but keeping it - // unconditional costs nothing and keeps the two branches comparable. + // Carry the parton line into the mother, which occupies slot particle1. int trace = cluster_trace[i] & 0x3; int a1 = rep1[particle1], a2 = rep2[particle1]; int b1 = rep1[particle2], b2 = rep2[particle2]; @@ -788,13 +715,8 @@ KERNELSPEC int mlm_clustering_walk( step_mother_leg2[i] = rep2[particle1]; } - // Any outgoing leg that no QCD clustering booked a scale onto keeps - // sqrt(s) rather than zero, mirroring the "ptclus = etot" fallback in - // Template/LO/SubProcesses/reweight.f. This covers the legs that are not - // jets at all, and jets whose winning clustering history happened to - // contain no QCD splitting. pt_clust is what an MLM veto compares against - // qcut, so a leg reported at zero would trip a veto that a leg with no - // clustering scale must never trip. + // Legs without a booked QCD scale get sqrt(s), as the "ptclus = etot" + // fallback in reweight.f; zero would wrongly trip an MLM veto. for (int i = 0; i < n_part - 2; ++i) { if (outgoing_scales[i] == 0.0) { outgoing_scales[i] = cm_energy; @@ -802,9 +724,7 @@ KERNELSPEC int mlm_clustering_walk( } if (cluster_max == 0) { - // A 2 -> 1 process has no clustering at all. cluster.f's special - // 2 -> 1 case takes pt2ijcl = pcl(4, imocl), the invariant mass squared - // of the outgoing line, and setclscales sets every scale from it. + // 2 -> 1: no clustering; use the outgoing mass as in cluster.f's special case. FourMom p_out{momenta[2][0], momenta[2][1], momenta[2][2], momenta[2][3]}; ren_scale_val = sqrt(max(lsquare(p_out), 0.0)); } else { @@ -815,42 +735,34 @@ KERNELSPEC int mlm_clustering_walk( } FVal fac_scale1 = fac_scale, fac_scale2 = fac_scale; - // One pdf reweighting slot per clustering that can sit on a beam line, - // plus one per beam for the 2 -> 1 root, which sits on both. A slot that - // no step claims stays inert: its two scales are equal, so its ratio is - // one, and its weight is one whatever the density comes out as. - // Which outgoing legs the merging cut applies to. Filled by madevent's - // iqjets walk below where that scale scheme is in use; without it the only - // thing available is the leg's own flavour and the vertex's. - // The scale each clustering is reweighted at. rewgt reads pt2ijcl *after* - // setclscales has overwritten it - at the central vertex with the emitted - // object's transverse mass, and at the last one to keep it above the first - // - so the coupling does not see the raw clustering measure. Defaults to - // that measure for the scheme that has no such table. + // The scale each clustering is reweighted at: rewgt reads pt2ijcl after + // setclscales has overwritten it, so this is not the raw clustering measure. + // Defaults to that measure when the madevent scale table is not built. FVal alphas_step[N_EXT_MAX - 3]; for (int i = 0; i < cluster_max; ++i) { alphas_step[i] = cluster_scales[i]; } + // Which outgoing legs the merging cut applies to; filled from iqjets by the + // madevent walk, otherwise the leg and vertex flavours are used. bool merging_jet[N_EXT_MAX]; bool have_merging_jets = false; for (int i = 0; i < n_part; ++i) { merging_jet[i] = false; } + // One pdf reweighting slot per clustering that can sit on a beam line, plus + // one per beam for the 2 -> 1 root. An unclaimed slot is inert (equal scales). int pdf_slot_count = n_part - 1; int rw_flavor[N_EXT_MAX - 1]; FVal rw_x[N_EXT_MAX - 1]; FVal rw_q_num[N_EXT_MAX - 1]; FVal rw_q_den[N_EXT_MAX - 1]; FVal rw_active[N_EXT_MAX - 1]; - // Which beam a slot sits on. Only the two root slots know that statically; - // for the rest it depends on which clustering won, so it has to travel - // with the slot. + // Which beam a slot sits on; known statically only for the two root slots. FVal rw_beam[N_EXT_MAX - 1]; - // The beam line's momentum fraction before the step's own z, which is - // what madevent books for its systematics (s_xpdf = xnow / zcl): they - // rebuild the ladder as f(x_i, Q_i) / f(x_i+1, Q_i) from those. + // Beam momentum fraction before the step's own z (s_xpdf = xnow / zcl in + // madevent), used by systematics to rebuild the ladder. FVal rw_x_prev[N_EXT_MAX - 1]; for (int i = 0; i < pdf_slot_count; ++i) { rw_flavor[i] = 0; @@ -863,31 +775,19 @@ KERNELSPEC int mlm_clustering_walk( } FVal pdf_scale_val[2] = {-1.0, -1.0}; - // pdf_reweighting == 2 under clustering_mean: walk the beam lines as - // madevent does below, but keep only the pdf half of it - the lowered - // leading scale, the ladder and the per-beam scale it ends at, which is - // also the factorisation scale then reported. Without it clustering_mean - // evaluates both beam densities at the softest clustering scale, with no - // ladder. + // pdf_reweighting == 2 under clustering_mean: do the madevent beam-line walk + // below but keep only its pdf part (ladder and per-beam final scale, which + // is also the reported factorisation scale). bool mean_with_ladder = scale_scheme != SCALES_MADEVENT && pdf_reweighting == 2; if (scale_scheme == SCALES_MADEVENT || mean_with_ladder) { - // Everything the walk writes besides the densities and mu_F - mu_R, - // the alpha_s vertex scales, the jet scales and the merging-jet gating - // - is madevent's only, and stays clustering_mean's under - // mean_with_ladder: every such write below sits under this flag. + // Writes other than the densities and mu_F (mu_R, alpha_s vertex scales, + // jet scales, merging-jet gating) happen only when this is set. bool madevent_scales = scale_scheme == SCALES_MADEVENT; - // madevent clusters one step further than this kernel does: its loop - // runs all the way to the 2 -> 1 configuration, so it has one more - // scale than cluster_max. That last step absorbs whatever is left of - // the final state into a beam, and its scale is the transverse mass of - // that object - for Drell-Yan, the boson's mT. It is also the step - // jcentral usually lands on, which is why madevent's factorisation - // scale comes out as mT of the lepton pair rather than a jet scale. - // - // Rather than extending the state machine, take that step here: with - // the walk finished exactly one final-state slot is still alive, and - // the clustering is fully determined by which beam it goes to. + // madevent clusters one step further (to 2 -> 1), absorbing what is left + // of the final state into a beam at its transverse mass (for Drell-Yan + // the boson mT). That step is taken here: exactly one final-state slot + // is still alive after the walk. int leftover = -1; for (int i = 2; i < n_part; ++i) { if (alive & (1 << i)) { @@ -895,11 +795,8 @@ KERNELSPEC int mlm_clustering_walk( break; } } - // cluster.f's pt2ijcl(n+1) = djb(pcl(imap(3,2))): the lab frame after a - // final-state last clustering (it boosts back first), the current frame - // after an initial-state one. In t t~ + jet with the jet taken into a - // beam first, that current frame is the one where t and t~ balance, so - // the root and the top's vertex carry the same scale and mu_R == mu_F. + // cluster.f's pt2ijcl(n+1) = djb(pcl(imap(3,2))): lab frame after a + // final-state last clustering, current frame after an initial-state one. FVal extra_scale = leftover < 0 ? FVal(0.0) : sqrt(djb_clus( @@ -923,13 +820,9 @@ KERNELSPEC int mlm_clustering_walk( qcdline[j] = (beam_flags >> (2 * j)) & 1; partonline[j] = qcdline[j]; } - // goodjet(i) of reweight.f: "every clustering this line has been - // through so far was a jet one". It starts as the flavour of each - // external - QCD for a beam, jet for an outgoing leg - and can only - // ever be cleared, so a line that once passed through a non-jet vertex - // never counts as a parton line again. Reading each daughter's flavour - // instead, which is what LINE_FLAVOR does, keeps beam lines alive past - // vertices where madevent would have stopped them. + // goodjet(i) of reweight.f: every clustering the line went through was a + // jet one. Starts from the external flavour (QCD for a beam, jet for an + // outgoing leg) and is only ever cleared. LINE_FLAVOR does not use it. bool goodjet[N_EXT_MAX]; for (int i = 0; i < n_part; ++i) { goodjet[i] = i < 2 ? ((beam_flags >> (2 * i)) & 1) != 0 @@ -937,9 +830,8 @@ KERNELSPEC int mlm_clustering_walk( } bool use_goodjet = parton_line_scheme == LINE_GOODJET; - // iqjets of reweight.f: which outgoing legs count as merging jets. - // This is the gate on the xqcut check, and the reason a jet emitted - // where no radiation happened is exempt from it. + // iqjets of reweight.f: which outgoing legs count as merging jets (the + // gate on the xqcut check). // // 1 certainly a jet: emitted while the beam line was still a // parton line, or a gluon, which has a soft singularity the @@ -947,10 +839,8 @@ KERNELSPEC int mlm_clustering_walk( // jcode possibly a jet, resolved at the end // 0 not a merging jet // - // jcode counts how many times the walk has crossed between QCD and - // non-QCD vertices, so that an emission can be placed relative to those - // crossings; increasecode makes the first jet vertex after a run of - // non-jet ones count as another crossing. + // jcode counts crossings between QCD and non-QCD vertices; increasecode + // makes the first jet vertex after non-jet ones count as another. int iqjets[N_EXT_MAX]; for (int i = 0; i < n_part; ++i) { iqjets[i] = 0; @@ -1021,12 +911,9 @@ KERNELSPEC int mlm_clustering_walk( step_daughter_goodjet[i][1] = goodjet[particle2]; bool is_jet_in = (trace & TRACE_IS_JET_IN) != 0; if (particle1 >= 2) { - // Final-state clustering. isjetvx() of reweight.f: a QCD - // vertex, and one where a jet actually came out - which it did - // if a jet daughter sits opposite either a jet mother or a - // mother repeating the other daughter's flavour, i.e. an - // emission off a line rather than a splitting into something - // else. + // Final-state clustering. isjetvx() of reweight.f: a QCD vertex + // where a jet daughter sits opposite a jet mother or a mother + // repeating the other daughter's flavour (an emission off a line). bool is_qcd_vertex = (data >> 27) & 1; bool is_jet1 = (data >> 28) & 1; bool is_jet2 = (data >> 29) & 1; @@ -1039,11 +926,8 @@ KERNELSPEC int mlm_clustering_walk( int mother_leg1 = step_mother_leg1[i]; int mother_leg2 = step_mother_leg2[i]; if (!is_jet_vertex) { - // "Remove non-gluon jets that lead up to non-jet - // vertices": a quark that was counted as a jet earlier but - // whose line runs into a vertex that made no jet was not a - // merging emission after all. A vertex whose three lines - // all carry colour, or none of them do, is left alone. + // "Remove non-gluon jets that lead up to non-jet vertices"; + // vertices with all or no lines coloured are left alone. if (!is_qcd_vertex && mother_leg1 >= 2) { bool leg1_octet = ((jet_leg_mask >> (mother_leg1 + LEG_OCTET_SHIFT)) & 1) != 0; @@ -1100,16 +984,12 @@ KERNELSPEC int mlm_clustering_walk( ); } if (leftover >= 0) { - // The 2 -> 1 root clusters both beams into what is left of the - // final state, so it lies on both beam lines and reweight.f walks - // it twice, once per beam. Seen from beam j the emitted object is - // that leftover and the mother is the *other* beam's parton, which - // is what decides whether either line carries on past the root. + // The 2 -> 1 root lies on both beam lines and is walked once per beam + // (as in reweight.f); the emitted object is the leftover and the + // mother is the other beam's line. bool leftover_jet = use_goodjet ? goodjet[leftover] : slot_is_jet[leftover]; for (int j = 0; j < 2; ++j) { - // Seen from beam j the mother is the other beam's line as it - // stands at the root, not the external parton it started as - // reweight.f reads ipdgcl(imo) with imo = idacl(n,3-i). walk_line( j, @@ -1123,9 +1003,7 @@ KERNELSPEC int mlm_clustering_walk( } } // "Emissions with code 1 are always jets; now take care of possible - // jets". Once a beam line has stopped being a parton line, everything - // tagged only provisionally and before the last crossing was not a - // merging emission. + // jets": drop provisional tags up to the last crossing. if (!partonline[0] || !partonline[1]) { if (partonline[0] || partonline[1]) { --jcode; @@ -1148,24 +1026,17 @@ KERNELSPEC int mlm_clustering_walk( } } - // The scale of each step, across the recorded history plus the extra - // 2 -> 1 one, as a mutable table because the central vertices are - // overwritten in place below just as pt2ijcl is in reweight.f. + // Scale of each step incl. the extra 2 -> 1 one; overwritten in place + // below like pt2ijcl in reweight.f. FVal pt_step[N_EXT_MAX - 2]; for (int i = 0; i < steps; ++i) { pt_step[i] = i == cluster_max ? extra_scale : cluster_scales[i]; } - // "If last clustering is s-channel QCD (e.g. ttbar) use mt2last - // instead" (setclscales in reweight.f): when the last recorded - // clustering joined two coloured final-state lines into a coloured - // one, and both beam lines are still parton lines at the root, the - // root's transverse mass becomes that average mT of the pair. Without - // it the root keeps the mT of the whole final state, which for - // q q~ > t t~ is sqrt(shat) - on average 2.3 times mT(t). reweight.f - // also writes it onto mt2ij of the last recorded clustering, but that - // one is final-state and jcentral only ever lands on an initial-state - // clustering or the root, so the root is the only place it is read. + // "If last clustering is s-channel QCD (e.g. ttbar) use mt2last instead" + // (setclscales in reweight.f): applies at the root when both beam lines + // are still parton lines. Only the root reads it, as jcentral is never a + // final-state clustering. FVal root_mt = 0.0; if (mt_last > 2.0 && cluster_max >= 1 && jlast[0] == cluster_max && jlast[1] == cluster_max && @@ -1173,18 +1044,14 @@ KERNELSPEC int mlm_clustering_walk( root_mt = mt_last; } - // "Set central scale to mT2": at an initial-state clustering the - // relevant scale is not the clustering measure but the transverse mass - // of what was emitted there. For Drell-Yan the central vertex is where - // the lepton pair attaches, which is why madevent's factorisation - // scale comes out as mT(ll) and not a jet scale. + // "Set central scale to mT2": at an initial-state clustering use the + // transverse mass of what was emitted (for Drell-Yan, mT of the leptons). for (int j = 0; j < 2; ++j) { if (jcentral[j] < 0) { continue; } - // At the root cluster.f takes mt2ij from daughter 2, which there - // is the second beam's parton: mT of a beam parton is zero, so - // the root is only overridden through mt2last above. + // At the root mt2ij is the second beam's mT (zero), so only mt2last + // overrides it. FVal mt = jcentral[j] == cluster_max ? root_mt : cluster_mt[jcentral[j]]; if (mt > 0.0) { @@ -1192,12 +1059,8 @@ KERNELSPEC int mlm_clustering_walk( } } - // "Ensure that last scales are at least as big as first scales". - // reweight.f raises the entry in pt2ijcl itself, so when jlast and - // jcentral are the same vertex - which is the usual case, both beam - // lines running QCD all the way to the root - the central scale is - // raised with it and mu_F comes out as that one scale rather than a - // geometric mean of two. Writing into pt_step keeps that. + // "Ensure that last scales are at least as big as first scales". Raised + // in the table itself, so a shared jlast/jcentral vertex is raised too. for (int j = 0; j < 2; ++j) { if (jlast[j] >= 0 && jfirst[j] >= 0 && pt_step[jfirst[j]] > pt_step[jlast[j]]) { @@ -1210,17 +1073,10 @@ KERNELSPEC int mlm_clustering_walk( } } - // The jet scales madevent writes, the "Store jet info for matching" - // loop at the end of setclscales. A leg only takes a scale from a jet - // vertex its line is still a good jet at, and a line stops being one - // at the first leg that iqjets does not call a merging jet. Everything - // else is written at sqrt(s), which is what tells the shower's MLM - // matching to leave that parton alone: without this a VBF tagging - // quark that radiated a gluon kept the scale of that emission, and was - // matched and vetoed as if it were a merging jet. - // - // Only the production scheme is madevent's; the emission scheme keeps - // the booking above. + // The "Store jet info for matching" loop of setclscales. A leg takes a + // scale only from jet vertices while its line is still a good jet (not + // yet a leg iqjets rejects); everything else is sqrt(s), which tells the + // shower matching to leave that parton alone. Production scheme only. if (by_production && madevent_scales) { FVal ptclus[N_EXT_MAX]; for (int leg = 0; leg < n_part; ++leg) { @@ -1281,15 +1137,11 @@ KERNELSPEC int mlm_clustering_walk( s_central[j] = jcentral[j] < 0 ? 0.0 : pt_step[jcentral[j]]; } - // mu_R: the geometric mean of the four scales. madevent writes it as - // (pt2 pt2 pt2 pt2)^(1/8) over squared scales, which is the same thing. + // mu_R: geometric mean of the four scales (madevent: (pt2 pt2 pt2 pt2)^(1/8)). FVal last_scale = pt_step[steps - 1]; - // madevent's mu_R, which mu_F falls back on; it only becomes the - // event's mu_R under madevent_scales + // Used as mu_F fallback; becomes the event's mu_R only under madevent_scales. FVal walk_ren_scale; - // Take the roots before multiplying. A degenerate clustering leaves a - // scale at SCALE_MAX, and the product of four of those overflows to - // infinity, which then reaches alpha_s as a NaN. + // Take roots before multiplying: four scales at SCALE_MAX would overflow. if (jlast[0] >= 0 && jlast[1] >= 0) { walk_ren_scale = pow(s_last[0], 0.25) * pow(s_central[0], 0.25) * pow(s_last[1], 0.25) * pow(s_central[1], 0.25); @@ -1318,16 +1170,9 @@ KERNELSPEC int mlm_clustering_walk( fac_scale2 = fac_scale1; } - // "Take care of case when jcentral are zero". - // - // The branch that ends that chain in reweight.f, capping each beam at - // min(pt2ijcl(jfirst), q2fact) when pdfwgt is on, is deliberately not - // ported. It exists so madevent can evaluate the PDF low on the - // clustering ladder and reweight back up; the central scale is kept in - // q2bck and restored, which is why turning pdfwgt off moves madevent's - // cross section (1113 -> 1093 pb for Z+0,1,2,3j) but leaves SCALUP - // unchanged. madspace has no such ladder reweighting, so the central - // scale is what both the PDF and the LHE should see. + // "Take care of case when jcentral are zero". The pdfwgt branch of + // reweight.f (capping each beam at min(pt2ijcl(jfirst), q2fact)) is not + // ported here; the ladder is handled by the pdf reweighting block below. if (jcentral[0] < 0 && jcentral[1] < 0) { if (!(fac_scale1 > 0.0) && !(fac_scale2 > 0.0)) { fac_scale1 = pt_step[steps - 1]; @@ -1362,26 +1207,14 @@ KERNELSPEC int mlm_clustering_walk( ren_scale_val = walk_ren_scale; } - // The pdf half of rewgt in Template/LO/SubProcesses/reweight.f, the - // other thing madevent does to a merged event that a single density at - // the factorisation scale does not. - // - // A merged event's beam density belongs at the scale of the emission - // that took the parton out of the beam, not at the scale of the hard - // process. madevent gets there in two moves: it evaluates the density - // low on the clustering ladder, at min(pt(jfirst), mu_F), and then - // walks each beam line back up, multiplying by f(x z, Q_i) / f(x z, - // Q_i-1) at every further clustering the line takes part in, with the - // momentum fraction rescaled by that clustering's z as it goes. The - // last step lands on mu_F itself, so the chain ends where the density - // would have been evaluated in the first place - except when the line - // has only one clustering on it, where the lowered scale stands with - // nothing to correct it. That asymmetry is madevent's, and it is a - // good part of why turning pdfwgt off moves the cross section at all. + // The pdf half of rewgt in Template/LO/SubProcesses/reweight.f. The + // density is first evaluated low on the ladder, at min(pt(jfirst), mu_F), + // then each beam line is walked up, multiplying by f(x z, Q_i) / + // f(x z, Q_i-1) per further clustering, with x rescaled by that + // clustering's z. A line with a single clustering keeps the lowered scale. // - // Only the scales, the momentum fractions and the flavour classes are - // decided here. Which density a class stands for is not knowable yet: - // the flavour is sampled from the very densities this is correcting. + // Only scales, momentum fractions and flavour classes are set here; the + // flavour is sampled later from the densities being corrected. bool reweight_pdf = pdf_reweighting != 0 && jcentral[0] >= 0 && jcentral[1] >= 0; pdf_scale_val[0] = fac_scale1; @@ -1393,10 +1226,8 @@ KERNELSPEC int mlm_clustering_walk( pdf_scale_val[j] = min(pt_step[jfirst[j]], q_central[j]); } } - // Where each beam's line has got to: its flavour class, the - // product of the z it has picked up, and the scale its density - // was last evaluated at. reweight.f keeps these as ibeam(j), - // xnow(j) and pt2pdf(ibeam(j)). + // Per beam: flavour class, product of z, and scale of the last density + // evaluation (ibeam(j), xnow(j), pt2pdf(ibeam(j)) in reweight.f). int line_class[2]; FVal x_frac[2] = {1.0, 1.0}; FVal pt_pdf[2] = {0.0, 0.0}; @@ -1406,22 +1237,18 @@ KERNELSPEC int mlm_clustering_walk( } for (int i = 0; i < steps; ++i) { for (int j = 0; j < 2; ++j) { - // A recorded clustering sits on the beam whose slot is its - // first daughter, and on no beam at all if that daughter is - // a final-state one. The root sits on both. + // A clustering sits on the beam of its first daughter (none if + // that is final-state); the root sits on both. bool at_root = i >= cluster_max; if (!at_root && (cluster_history[i] & 0xFF) != j) { continue; } - // Once a line stops being a parton reweight.f stops - // advancing ibeam(j), so no later clustering can match it - // again and the chain is over for good. + // Once a line stops being a parton the chain is over. if (line_class[j] < 0) { continue; } int slot = at_root ? cluster_max + j : i; - // zcl is 1 at the root, so the momentum fraction is only - // ever rescaled by the recorded clusterings. + // zcl is 1 at the root. FVal z = at_root ? FVal(1.0) : cluster_z[i]; FVal x_prev = x_frac[j]; if (z > 0.0 && z < 1.0) { @@ -1455,15 +1282,9 @@ KERNELSPEC int mlm_clustering_walk( } } - // The merging cut. A matrix-element jet below xqcut is radiation the parton - // shower is meant to produce, so the event is dropped - but only for a leg - // that is a merging jet in the first place. madevent decides that with - // iqjets, which is only ever set for a leg emitted at a jet vertex; a leg - // it leaves at zero is exempt however soft the vertex it sits on. Without - // that gate a jet is rejected at vertices that produced no radiation at - // all, most visibly a quark pairing into a W, whose measure is a - // lepton-side kt with nothing to do with the jet's own transverse - // momentum. + // The merging cut (reweight.f "Check xqcut for vertices with jet daughters + // only"): the event fails if a bare outgoing merging jet sits at a + // clustering below xqcut. Legs with iqjets == 0 are exempt. bool passes_xqcut = true; if (FVal(xqcut) > 0.0) { for (int i = 0; i < cluster_max; ++i) { @@ -1492,26 +1313,13 @@ KERNELSPEC int mlm_clustering_walk( ren_scale = ren_scale_val; fact_scale1 = fac_scale1; fact_scale2 = fac_scale2; - // A weight rather than a flag, so that it can simply multiply the event - // weight the way every other cut in madspace does. // Scale of each clustering vertex for the alpha_s reweighting, following // the loop in Template/LO/SubProcesses/reweight.f: every clustering except - // the last one is reweighted by alphas(pt_clust) / alphas(mu_R), and only - // where a parton is produced. A vertex that is not reweighted is handed - // mu_R itself, so its ratio is exactly one and the consumer needs no mask. - // - // madevent gates this on goodjet / ispartonvx; the QCD flag of the vertex - // is the same statement for every case that arises here - all three lines - // coloured - and is what the rest of this kernel already uses. - // - // reweight.f drops an event outright when a reweighted vertex sits at or - // below 2 GeV, where the coupling is not to be trusted; alphas_weight is - // that veto. - // - // ALPHAS_GEOMETRIC hands every reweighted vertex the same scale, the - // geometric mean of the individual ones, so the product below turns into - // alphas()^n. That is the cheaper approximation of the same idea: one - // coupling for the whole ladder rather than one per rung. + // the last one (as in cluster_max) is reweighted by alphas(pt_clust) / + // alphas(mu_R), where a parton is produced (taken here as the QCD flag). + // reweight.f drops an event if a reweighted vertex is at or below 2 GeV; + // alphas_weight is that veto. ALPHAS_GEOMETRIC gives every reweighted + // vertex the geometric mean of their scales. bool alphas_ok = true; FVal log_sum = 0.0; int reweighted = 0; @@ -1528,10 +1336,8 @@ KERNELSPEC int mlm_clustering_walk( } FVal mean_scale = reweighted > 0 ? exp(log_sum / reweighted) : ren_scale_val; - // A vertex left at mu_R is handed 0 rather than mu_R itself: a reweighted - // vertex can sit exactly at mu_R (one clustering under clustering_mean), and - // the systematics have to tell the two apart, since madevent varies a - // reweighted vertex with alpsfact and the others with mu_R. + // A vertex that is not reweighted gets 0 rather than mu_R, so that + // systematics can tell it from a reweighted vertex sitting at mu_R. for (int i = 0; i < cluster_max; ++i) { bool is_qcd_vertex = (cluster_history[i] >> 27) & 1; if (alphas_scheme == ALPHAS_NONE || !is_qcd_vertex) { @@ -1543,14 +1349,12 @@ KERNELSPEC int mlm_clustering_walk( } } - // Kept apart from xqcut_weight: that one is the merging cut and nothing - // else, and only the reweighting cares where the coupling stops being - // usable. + // Weights (0 or 1) that multiply the event weight; alphas_weight is kept + // apart from the merging cut xqcut_weight. alphas_weight = (alphas_scheme == ALPHAS_NONE || alphas_ok) ? 1.0 : 0.0; xqcut_weight = passes_xqcut ? 1.0 : 0.0; - // Any scale scheme other than madevent's has no clustering ladder to walk, - // so the density stays where the factorisation scale put it. + // Without a ladder walk the density stays at the factorisation scale. if (!(pdf_scale_val[0] > 0.0)) { pdf_scale_val[0] = fac_scale1; } @@ -1561,8 +1365,8 @@ KERNELSPEC int mlm_clustering_walk( pdf_scale2 = pdf_scale_val[1]; for (int i = 0; i < pdf_slot_count; ++i) { if (rw_active[i] == 0.0) { - // Somewhere the density is defined, so that an inert slot cannot - // trip the low-density veto the consumer applies. + // Keep an inert slot at a valid scale so the consumer's low-density + // veto cannot trip. rw_q_num[i] = pdf_scale_val[0]; rw_q_den[i] = pdf_scale_val[0]; } @@ -1582,10 +1386,9 @@ KERNELSPEC int mlm_clustering_walk( } diagram_index = state_machine[state + rand_index + 1]; - // How many outgoing legs this history calls merging jets: iqjets where the - // madevent scale scheme has filled it, a jet emitted at a QCD vertex - // otherwise - the same test the merging cut above falls back to. This is - // njets of setclscales, which HISTORY_MADEVENT compares between histories. + // Number of merging jets: iqjets where the madevent walk filled it, else + // jets emitted at QCD vertices. This is njets of setclscales, compared + // between histories under HISTORY_MADEVENT. int merging_jet_count = 0; if (have_merging_jets) { for (int leg = 2; leg < n_part; ++leg) { @@ -1711,11 +1514,8 @@ KERNELSPEC void mlm_clustering( int start = start_state; if (history_mode == HISTORY_MADEVENT && start != 0) { // setclscales clusters over every graph first and keeps that history - // unless its jet count differs from the one stored for the channel, - // which was itself clustered along the channel's graph. Every walk - // writes all outputs, so the one that ran last is the one that stands: - // the channel's walk is kept aside and put back when it wins, rather - // than walked a second time. + // unless its jet count differs from the channel's graph. Both walks write + // all outputs, so the channel walk's outputs are saved and restored if it wins. int diagram_jets = walk(start); int n_part = momenta.size(); FVal kept_ren = ren_scale, kept_fact1 = fact_scale1, diff --git a/madspace/src/kernels/multichannel.hpp b/madspace/src/kernels/multichannel.hpp index 1c37f4a3c8..3997220020 100644 --- a/madspace/src/kernels/multichannel.hpp +++ b/madspace/src/kernels/multichannel.hpp @@ -25,16 +25,7 @@ KERNELSPEC void kernel_collect_channel_weights( norm = norm + amp2_val; channel_weights[chan_index] += amp2_val; } - // Every amplitude can vanish at once -- a channel whose matrix element is - // identically zero (an FCNC subprocess whose Wilson coefficients are all zero, - // say) has amp2 == 0 for every diagram, so norm == 0. Dividing by it would give - // 0/0 = nan for every channel weight, and that nan reaches the event weight and - // aborts the whole integration instead of contributing nothing. Fall back to the - // uniform distribution: the channel weights have to stay a normalised - // distribution over the channels, and with no amplitude to prefer one, none is - // preferred. The event weight is zero either way, since the amplitudes are. - // norm is substituted before the division rather than the result being repaired - // after it, so that the degenerate 0/0 is never formed in the first place. + // If all amplitudes vanish, norm == 0; use uniform weights instead of 0/0 = nan. auto degenerate = norm == 0.; auto safe_norm = where(degenerate, FVal(1.), norm); FVal uniform_weight(1. / static_cast(channel_weights.size())); diff --git a/madspace/src/kernels/operations.hpp b/madspace/src/kernels/operations.hpp index b129ef6e8f..9c77aad0b4 100644 --- a/madspace/src/kernels/operations.hpp +++ b/madspace/src/kernels/operations.hpp @@ -73,10 +73,8 @@ void backward_batch_foreach( discard_grad = instruction.discard_input_grad[i]; } if (discard_grad) { - // The gradient of this input is never used (literal or integer). A - // batch-size-one gradient would be a broadcast output, which forces the - // scalar kernel on a single thread, so write it to batch-sized scratch - // memory instead. + // Unused gradient (literal or integer input): use batch-sized scratch, as a + // batch-size-one broadcast output would force a single-threaded scalar kernel. auto& input = locals[input_index]; if (!input_grad || input_grad.size(0) != batch_size) { Sizes shape = input.shape(); @@ -86,16 +84,12 @@ void backward_batch_foreach( } } else if (!input_grad) { auto& input = locals[input_index]; - /*if (input.size(0) != batch_size && input.dtype() == DataType::dt_float) { - throw std::runtime_error("backward not possible for broadcasting args"); - }*/ input_grad = Tensor( input.dtype(), input.shape(), device, instruction.input_grad_alloc_hints[i] ); - // input_grad.zero(device); } input_grads[i] = &input_grad; } @@ -140,7 +134,6 @@ void backward_op_stack( device, instruction.input_grad_alloc_hints[i] ); - // input_grad.zero(device); } ++i; } @@ -191,7 +184,6 @@ void backward_op_unstack( device, instruction.input_grad_alloc_hints[0] ); - // input_grad.zero(device); } device.sync_barrier(); auto unstacked_grads = input_grad.unstack(1); @@ -221,7 +213,6 @@ void backward_op_pop( device, instruction.input_grad_alloc_hints[0] ); - // input_grad.zero(device); } device.sync_barrier(); std::size_t last_index = input_grad.size(1) - 1; @@ -274,7 +265,6 @@ void backward_op_batch_cat( device, instruction.input_grad_alloc_hints[i] ); - // input_grad.zero(device); } ++i; } @@ -310,7 +300,6 @@ void backward_op_batch_split( device, instruction.input_grad_alloc_hints[0] ); - // input_grad.zero(device); } device.sync_barrier(); auto split_grads = input_grad.split(0, sizes); @@ -362,7 +351,6 @@ void backward_op_cat( device, instruction.input_grad_alloc_hints[i] ); - // input_grad.zero(device); } ++i; } @@ -476,7 +464,6 @@ void backward_op_rqs_reshape( device, instruction.input_grad_alloc_hints[0] ); - // input_grad.zero(device); } device.sync_barrier(); auto input_grad_reshaped = input_grad.factor_dim(1, n_dims); diff --git a/madspace/src/kernels/rambo.hpp b/madspace/src/kernels/rambo.hpp index 21e8cd614c..0aea2b5005 100644 --- a/madspace/src/kernels/rambo.hpp +++ b/madspace/src/kernels/rambo.hpp @@ -316,7 +316,7 @@ KERNELSPEC void fast_rambo_massive_body_inverse( // Kernels -// massles +// massless template KERNELSPEC void kernel_fast_rambo_massless( @@ -342,7 +342,7 @@ KERNELSPEC void kernel_fast_rambo_massless_com( fast_rambo_massless_body(r, e_cm, p_out, det, p0); } -// masssive +// massive template KERNELSPEC void kernel_fast_rambo_massive( diff --git a/madspace/src/kernels/scale.hpp b/madspace/src/kernels/scale.hpp index 80053bf35f..a54bae1388 100644 --- a/madspace/src/kernels/scale.hpp +++ b/madspace/src/kernels/scale.hpp @@ -9,11 +9,7 @@ namespace kernels { template KERNELSPEC FVal transverse_mass(FIn momenta) { - // Returns the sum of the transverse masses sqrt(E^2 - pz^2) of the final - // state (NOT squared): the callers below square it to form the scale^2 - // (= mu^2), exactly like kernel_scale_transverse_energy. Returning the - // square here would double-square it into mu^4 and send the PDF/alphaS - // lookup far out of range (NaN). + // Sum over final-state particles (index >= 2) of sqrt(E^2 - pz^2), not squared. FVal mt_sum(0.); for (std::size_t i = 2; i < momenta.size(); ++i) { auto p = momenta[i]; diff --git a/madspace/src/kernels/threeparticle.hpp b/madspace/src/kernels/threeparticle.hpp index 3d64c7101a..4ab91d0d8c 100644 --- a/madspace/src/kernels/threeparticle.hpp +++ b/madspace/src/kernels/threeparticle.hpp @@ -171,9 +171,8 @@ three_body_decay_inverse(FourMom p1, FourMom p2, FourMom p3) { return {r_e1, r_e2, r_phi, r_cos_theta, r_beta, m0, m1, m2, m3, 1. / det}; } -// Width given to an empty s23 range, relative to s23: an absolute EPS is below -// one ulp for s23 > ~1e4 and left max == min, and a few ulps still make the -// differences of powers in the 1/s^nu sampling vanish. +// Width given to an empty s23 range, relative to s23, since an absolute EPS can be +// below one ulp of s23. template KERNELSPEC FVal s23_min_width(FVal s) { return max(FVal(EPS), 1e-10 * fabs(s)); @@ -194,11 +193,8 @@ KERNELSPEC Pair, FVal> s23_min_max( // (the frame of rotate_two_ref), // s23 = m0^2 + m1^2 - 2 [(sqrt(s12) + E3) E1 - p3_z p1_z] // + 2 p3_x p1_t cos(phi), - // a range of width 4 p3_x p1_t about the phi-independent part. Every factor - // of the width is read off momenta, so it keeps its relative precision when - // a particle is soft. The Byckling-Kajantie form of the same range, through - // 3x3 Gram determinants, cancels down to the square of the width and loses - // it all below a softness of about 1e-8. + // so the range has width 4 p3_x p1_t. Every factor of the width is read off + // momenta, which keeps its relative precision when a particle is soft. FourMom p_tot; for (int i = 0; i < 4; ++i) { p_tot[i] = p_12[i] + p3[i]; @@ -469,11 +465,8 @@ KERNELSPEC Pair, FVal> s23_etmin_clamp( auto smax_b = m2_2 + m3_2 + 2. * e_eff * pim1_0 + 2. * sqrt(max(disc, 0.)) * pim1_1; auto smax_ok = active & (e_eff > 0.) & (disc > 0.) & (smax_b > smn); auto smx_new = where(smax_ok, min(smx, smax_b), smx); - // smin: a massless peeled particle needs ET -> minimum s-channel mass. - // Massless is judged relative to the energy scale: the inverse reads m2_2 - // off momenta, where a massless particle comes out at +-1e-16 E^2 or so, - // and an absolute test (m2_2 < EPS) turned this bound off there and - // changed the sampled range between forward and inverse. + // smin: a massless peeled particle needs ET -> minimum s-channel mass. Masslessness + // is judged relative to the energy scale, as m2_2 read off momenta is only ~1e-16 E^2. auto smin_b = 2. * etmin_2 * (pim1_0 - pim1_1 * cos(drcut)); auto massless_2 = m2_2 < 1e-10 * piir_0 * piir_0; auto smin_ok = active & massless_2 & (smin_b > smn) & (smin_b < smx_new); @@ -482,9 +475,9 @@ KERNELSPEC Pair, FVal> s23_etmin_clamp( return {smn_new, smx_new}; } -// Clamp the s23 invariant-mass range against the cut-derived lower bound. -// s23_min_cut is the minimum invariant mass^2 of the (2,3) system implied by -// the pt / m_inv_min / dR cuts (gen23's invm_min). +// Clamp the s23 range against the lower bound s23_min_cut (minimum mass^2 of the +// (2,3) system implied by the pt / m_inv_min / dR cuts, gen23's invm_min) and +// the ETmin refinement of s23_etmin_clamp. template KERNELSPEC void kernel_s23_min_max_cut( FIn pa, @@ -599,26 +592,19 @@ KERNELSPEC void kernel_s23_value_and_min_max_cut( // Arcsine map of the s23 sampling variable. // -// At fixed s12, t1 and t2 the invariant s23 is linear in cos(phi) on its -// physical range [s_phys_min, s_phys_max], and the 2->3 measure is flat in phi: -// ds23 / (8 sqrt(-G4)) = dphi / (2 sqrt(lambda)). -// Sampling s23 (or any smooth function of it) with a density that stays finite -// at the edges of the physical range therefore leaves an integrable -// 1/|sin(phi)| in the weight, with a log-divergent variance. With -// u = (s23 - s_phys_min) / (s_phys_max - s_phys_min) = sin^2(phi / 2), -// the map below is flat in theta = phi / 2 on the part [theta_a, theta_b] of -// [0, pi/2] that the sampling range [s_min, s_max] covers, and returns the -// position x in [0, 1] of the point in that range: +// At fixed s12, t1 and t2, s23 is linear in cos(phi) on its physical range +// [s_phys_min, s_phys_max] and the 2->3 measure is flat in phi. A sampling density +// that stays finite at the edges of that range thus leaves an integrable +// 1/|sin(phi)| in the weight with a log-divergent variance. With +// u = (s23 - s_phys_min) / (s_phys_max - s_phys_min) = cos^2(phi / 2), +// the map is flat in theta = (pi - phi) / 2 on the part [theta_a, theta_b] of [0, pi/2] +// covered by the sampling range [s_min, s_max], and gives the position x in [0, 1]: // x = (sin^2 theta - sin^2 theta_a) / (sin^2 theta_b - sin^2 theta_a), // theta = theta_a + (theta_b - theta_a) r. -// x is then handed to the s23 importance sampling in place of r (see -// kernel_s23_arcsine_sample, which does both in one step). Its Jacobian -// dx/dr vanishes like |sin(phi)| at a physical edge, so the product with the -// 2->3 Jacobian stays bounded; at an edge set by a cut instead (theta_a > 0 or -// theta_b < pi/2) it stays finite and nothing is cancelled. Differences of -// sin^2 are written as sin(B - A) sin(B + A), so that x keeps its relative -// precision where it is small, also when the sampled range is a narrow part of -// the kinematic one. +// dx/dr vanishes like |sin(phi)| at a physical edge, which cancels the 1/|sin(phi)| +// (not at an edge set by a cut). Differences of sin^2 are written as +// sin(B - A) sin(B + A) to keep relative precision. This function returns +// theta_a and theta_b; kernel_s23_arcsine_sample applies the map. template KERNELSPEC Pair, FVal> s23_arcsine_angles( FVal s_min, FVal s_max, FVal s_phys_min, FVal s_phys_max @@ -655,21 +641,14 @@ KERNELSPEC void kernel_s23_position( u_c = min(max((hi - s23_val) / width_safe, 0.), 1.); } -// Arcsine map in phi composed with the s23 importance sampling of @ref -// Invariant (flat for power 0, logarithmic for power 1, 1/(s - m^2)^power -// otherwise, with the same regularisation m^2 -> m^2 - 1e-2), in one step: -// r -> theta -> x, 1 - x -> s23 - s_min, s_max - s23 -> u, 1 - u. Every -// quantity is carried as a pair of distances to both ends of its range, with -// the power maps written through expm1 and log1p, so that u and 1 - u keep -// their relative precision down to the edges of the kinematic range. Going -// through s23 itself loses that: s23 - s_min is only known to ~1e-16 s. The -// forward/inverse round trip then only recovers r_s23 to ~1e-8 near the -// edges, which is where the arcsine map puts its points. +// Power-law s23 importance sampling (flat for power 0, logarithmic for power 1, +// 1/(s - m^2)^power otherwise, with m^2 -> m^2 - 1e-2) from x and 1 - x. Returns +// s23 - s_min, s_max - s23 and d s23 / d x. Both distances are computed through +// expm1 and log1p so that they keep relative precision at the range edges. template KERNELSPEC Triplet, FVal, FVal> s23_power_offsets( FVal x, FVal x_c, FVal s_min, FVal s_max, FVal power, FVal mass ) { - // returns s23 - s_min, s_max - s23 and d s23 / d x auto m2 = mass * mass - 1e-2; auto qa = max(s_min - m2, EPS2); auto qb = max(s_max - m2, EPS2); @@ -699,6 +678,10 @@ KERNELSPEC Triplet, FVal, FVal> s23_power_offsets( return {max(lo, 0.), max(hi, 0.), det}; } +// Arcsine map in phi (see s23_arcsine_angles) composed with the s23 importance +// sampling (s23_power_offsets) in one step, r -> x -> s23. It returns u and 1 - u +// of s23 in its physical range, from separate distances to both ends, since going +// through s23 itself would lose relative precision near the edges. template KERNELSPEC void kernel_s23_arcsine_sample( FIn r, @@ -825,9 +808,8 @@ KERNELSPEC void kernel_two_to_three_particle_scattering( FOut p2, FOut det ) { - // p_12 = p1 + p2 comes in directly rather than as pa + pb - p3: in a long - // chain it is a soft system next to the beams, and that difference would - // leave it with an absolute error of order the beam energy. + // p_12 is an input rather than pa + pb - p3, which would carry an absolute error + // of order the beam energy when p_12 is soft. auto p_12 = load_mom(p12); FourMom p_c; for (int i = 0; i < 4; ++i) { @@ -838,25 +820,16 @@ KERNELSPEC void kernel_two_to_three_particle_scattering( auto s12 = lsquare(p_12); auto t2 = lsquare(p_c); - // s23 is linear in cos(phi) across its kinematic range, and the 4x4 Gram - // determinant of Byckling-Kajantie factorises on that range: - // cos(phi) = 2 u - 1, u = (s23 - s23_min) / (s23_max - s23_min), - // -G4 = lambda(s12, ma^2, t2) / 16 * (s23 - s23_min) * (s23_max - s23). - // Both are evaluated through u and 1 - u, which come in as inputs (see - // kernel_s23_position and kernel_s23_arcsine_sample). Forming G4 (or the equivalent - // cos(phi) expression) from the invariants instead cancels terms of order s^4 down - // to a result that can be many orders of magnitude smaller whenever the - // s23 range is narrow (a soft particle), and returned Jacobians of up to - // 1e20 times the typical weight. The range itself comes from momenta for - // the same reason, see s23_min_max. + // On the s23 range, cos(phi) = 2 u - 1 with u = (s23 - s23_min) / (s23_max - s23_min) + // and -G4 = lambda(s12, ma^2, t2) / 16 * (s23 - s23_min) * (s23_max - s23). Both are + // evaluated through u and 1 - u (inputs) and a range from momenta (s23_min_max), + // as forming G4 from the invariants cancels catastrophically for a soft particle. auto s23_range = s23_min_max(load_mom(pa), load_mom(p3), p_12, s12, t1_abs, m1, m2); auto s23_width = s23_range.second - s23_range.first; FVal u_val(u), u_c_val(u_c); - // cos(phi) = u - (1 - u), |sin(phi)| = 2 sqrt(u (1 - u)), both at full - // relative precision near phi = 0, pi. u and 1 - u come from separate - // differences; dividing by their sum keeps cos^2 + sin^2 = 1 to rounding, - // so that the rotation does not change |p1|. + // cos(phi) = u - (1 - u), |sin(phi)| = 2 sqrt(u (1 - u)); dividing by u + (1 - u) + // keeps cos^2 + sin^2 = 1 to rounding so that the rotation preserves |p1|. auto u_sum = u_val + u_c_val; auto u_sum_safe = where(u_sum > 0., u_sum, FVal(1.)); auto cos_phi = (u_val - u_c_val) / u_sum_safe; @@ -865,7 +838,7 @@ KERNELSPEC void kernel_two_to_three_particle_scattering( // 8 sqrt(-G4) = sqrt(lambda) * (s23_max - s23_min) * |sin(phi)| auto sqrt_neg_gram4_x8 = sqrt(max(kaellen(s12, ma_2, t2), 0.)) * s23_width * sin_phi_abs; - // The edges of the range (sin(phi) = 0) are a set of measure zero. + // zero weight at the edges of the range (sin(phi) = 0) auto det_2to3 = where(sqrt_neg_gram4_x8 > 0., 1. / sqrt_neg_gram4_x8, FVal(0.)); auto scatter_out = @@ -873,10 +846,7 @@ KERNELSPEC void kernel_two_to_three_particle_scattering( auto p1_com = scatter_out.first; auto p3_p12 = boost(load_mom(p3), p_12, -1.); auto p1_rot = rotate_two_ref(p1_com, pa_com, p3_p12); - // p1 and p2 back to back in the p_12 rest frame, the softer one boosted on - // its mass shell and the other p_12 minus it (see boost_two_body). Always - // forming p2 = p_12 - p1 left a soft p2 off shell by up to a few 1e-5 of - // its energy. + // p1 and p2 back to back in the p_12 rest frame, boosted as in boost_two_body auto m12_rest = sqrt(max(s12, EPS2)); auto e2_com = 0.5 * (m12_rest - (m1 - m2) * (m1 + m2) / m12_rest); FourMom p2_rot{max(e2_com, 0.), -p1_rot[1], -p1_rot[2], -p1_rot[3]}; @@ -901,9 +871,8 @@ KERNELSPEC void kernel_two_to_three_particle_scattering_inverse( FOut u_c, FOut det ) { - // p_12 = p1 + p2 comes in directly rather than as pa + pb - p3: in a long - // chain it is a soft system next to the beams, and that difference would - // leave it with an absolute error of order the beam energy. + // p_12 is an input rather than pa + pb - p3, which would carry an absolute error + // of order the beam energy when p_12 is soft. auto p_12 = load_mom(p12); FourMom p_c; for (int i = 0; i < 4; ++i) { diff --git a/madspace/src/kernels/twoparticle.hpp b/madspace/src/kernels/twoparticle.hpp index 66815f1ca1..ceb392cda8 100644 --- a/madspace/src/kernels/twoparticle.hpp +++ b/madspace/src/kernels/twoparticle.hpp @@ -21,14 +21,9 @@ two_body_decay(FVal r_phi, FVal r_cos_theta, FVal m0, FVal m1, FVal< auto pp_open = 0.5 * where(m1 * m2 == 0., m0 - fabs(ed), sqrt(max(pp2, EPS))); auto e1_open = 0.5 * (m0 + ed); - // Below threshold (m0 < m1 + m2) the decay has no phase space, but the formulas - // above return a spurious positive weight: pp goes negative and e1 gets clamped - // to 0 while the spatial components don't, producing a spacelike p1 that blows - // up downstream (e.g. in boost()) into NaNs. pp2 > 0 is the exact Kaellen-function - // threshold test; pp_open > 0 also excludes it since the massless shortcut can - // return a negative pp there. Below threshold we substitute a valid - // massless-massless splitting so the kinematics stay physical, and force det = 0 - // so it contributes nothing to the integral. + // Below threshold (m0 < m1 + m2) there is no phase space: substitute a massless + // splitting to keep p1 physical (no NaNs in boost) and set det = 0. pp2 > 0 is + // the threshold test; pp_open > 0 covers the massless shortcut going negative. auto open = (pp2 > 0.) & (pp_open > 0.); auto pp = where(open, pp_open, 0.5 * m0_clip); auto e1 = where(open, e1_open, 0.5 * m0_clip); diff --git a/madspace/src/phasespace/channel_weight_network.cpp b/madspace/src/phasespace/channel_weight_network.cpp index 343468299f..2fc55d7c37 100644 --- a/madspace/src/phasespace/channel_weight_network.cpp +++ b/madspace/src/phasespace/channel_weight_network.cpp @@ -58,9 +58,6 @@ NamedVector ChannelWeightNetwork::build_function_impl( ) const { auto mask = fb.global(_mask_name, DataType::dt_float, {static_cast(_channel_count)}); - // auto net_input = - // _preprocessing.build_function(fb, {args["momenta"], args["x1"], args["x2"]}); - // auto net_output = _mlp.build_function(fb, net_input.values()).at(0); auto net_output = _mlp.build_function(fb, {args["input"]}).at(0); net_output = fb.scaled_tanh(net_output); return { diff --git a/madspace/src/phasespace/color_ordered_mapping.cpp b/madspace/src/phasespace/color_ordered_mapping.cpp index 2022a253ae..b32378e3bf 100644 --- a/madspace/src/phasespace/color_ordered_mapping.cpp +++ b/madspace/src/phasespace/color_ordered_mapping.cpp @@ -375,8 +375,7 @@ ColorOrderedMapping::ColorOrderedMapping( for (std::size_t i = 0; i < total; ++i) { input_types.push_back(std::format("random{}", i), batch_float); } - // Opt-in discrete channel: one int per 2->3 peel (the two-solution - // choice), appended after the continuous randoms. + // One int per 2->3 step (the two-solution choice), after the randoms. for (std::size_t j = 0; j < n_discrete; ++j) { input_types.push_back(std::format("discrete{}", j), batch_int); } @@ -673,13 +672,10 @@ Mapping::Result ColorOrderedMapping::build_forward_impl( Value R_b_for_set1 = fb.sub(pb, P_set2); Value R_b_for_set2 = fb.sub(pa, P_set1); - // Phase 1b: pre-sample intermediate rest masses for each walk. - // For each step j in 0..k-3 (last is the residual mass(s[k-1])): - // m_min = sum masses [set[j+1]..set[k-1]] - // m_max = m_rest_j - mass[set[j]], m_rest_0 = m_set - // Sampling from [m_min, m_set - sum_peeled] independently would - // over-count by (k-2)! by ignoring the monotonic ordering. - // Sampled after the central block: DoubleT only fixes m_set then. + // Phase 1b: intermediate rest masses of each walk, for steps j in 0..k-3. + // Each is bounded by the previous one (rest_mass_range), which avoids + // (k-2)! over-counting. Sampled after the central block, since DoubleT + // only fixes m_set there. auto sample_intermediate_masses = [&](const std::vector& s, Value m_set) -> ValueVec { std::size_t k = s.size(); @@ -768,8 +764,8 @@ Mapping::Result ColorOrderedMapping::build_forward_impl( Value im1, chain; bool first = true; for (std::size_t j = 0; j < k - 1; ++j) { - // New-rest mass: intermediate (rest_masses[j]) or final residual (j == - // k-2). + // Rest mass after this peel: rest_masses[j], or the last particle's + // mass for j == k-2. Value m_rest = (j < k - 2) ? rest_masses[j] : m_out.at(s.at(k - 1)); Value m_peel = m_out.at(s.at(j)); // Block convention: the first output is the chain (mass m_rest), @@ -1068,8 +1064,8 @@ Mapping::Result ColorOrderedMapping::build_inverse_impl( Value R_b_for_set1 = fb.sub(pb, P_set2); Value R_b_for_set2 = fb.sub(pa, P_set1); - // Phase 3 inverse: peel-off walks (mirror the forward restructure: first peel - // projects the own-side beam R_a, 2->3 peels project beam_other = 3-i side). + // Phase 3 inverse: peel-off walks, mirroring the forward (first peel projects + // the own-side beam R_a, 2->3 peels project beam_other). auto walk_inverse = [&](const std::vector& s, std::size_t k_other, diff --git a/madspace/src/phasespace/cross_section.cpp b/madspace/src/phasespace/cross_section.cpp index bea2fbabbb..c0b611e7ad 100644 --- a/madspace/src/phasespace/cross_section.cpp +++ b/madspace/src/phasespace/cross_section.cpp @@ -115,9 +115,7 @@ NamedVector DifferentialCrossSection::build_function_impl( std::array x1x2{1., 1.}; if (_decay) { - // No beams: nothing to extract momentum fractions from. The declared - // argument list still carries x1/x2 when input_momentum_fraction is on, - // so consume them and ignore their (unit) values. + // skip the declared x1/x2 arguments; the momentum fractions stay 1 if (_input_momentum_fraction) { arg_index += 2; } @@ -167,11 +165,7 @@ NamedVector DifferentialCrossSection::build_function_impl( std::size_t me_index = search - _matrix_element.outputs().begin(); Value matrix_element = me_result.at(me_index); if (_decay) { - // Differential decay rate: dGamma = |M|^2 dPhi_n / (2 M), in GeV. The - // flux is a compile-time constant here (M is fixed by the decaying - // particle), so this is a single multiply and needs no dedicated - // instruction. Deliberately no GeV^-2 -> pb conversion: a width is not - // a cross section. + // dGamma = |M|^2 dPhi_n / (2 M), in GeV, with no GeV^-2 -> pb conversion me_result.at(me_index) = fb.mul(me_result.at(me_index), 1. / (2. * _e_cm)); } else { diff --git a/madspace/src/phasespace/cuts.cpp b/madspace/src/phasespace/cuts.cpp index 64a408f4ce..57acb42434 100644 --- a/madspace/src/phasespace/cuts.cpp +++ b/madspace/src/phasespace/cuts.cpp @@ -68,12 +68,10 @@ Cuts::build_function_impl(FunctionBuilder& fb, const NamedVector& args) c namespace { -// The accessors below hand bounds to the phase-space mappings, which apply each -// one as a hard floor on a fixed particle or pair. That is only right for a -// bound every selected object has to satisfy: an ordered selection ("the -// leading jet") names a rank rather than a particle, and with CutMode::any a -// single object passing is enough, so neither says anything about a given -// particle unless the selection holds exactly one. Such cuts stay filters. +// The accessors hand bounds to the mappings as hard floors on a fixed particle +// or pair, which is only valid if every selected object must satisfy the bound. +// Ordered selections (a rank, not a particle) and CutMode::any cuts over +// several objects stay filters. bool binds_each_object(const Cuts::CutItem& item, std::size_t object_count) { return !item.observable.ordered() && (item.mode == Cuts::CutMode::all || object_count == 1); @@ -149,12 +147,10 @@ namespace { using PairList = std::vector>; -// The pairs, in the observable's own (full, beams included) indexing, whose -// invariant mass an observable measures: the genuine pair observables, and -// "mass" of summed momenta when that sum is a pair. With one group -// ("lepton-sum-mass") the sum is a pair only when the group holds exactly two -// particles; with two groups ("jet-jet-sum-mass") it is one pair per -// combination the groups can form. Empty for anything else. +// The pairs (indices including the beams) whose invariant mass an observable +// measures: the pair observables, and "mass" of summed momenta when the sum is +// a pair (one group of exactly two particles, or one pair per element of two +// groups). Empty for anything else. PairList mass_pairs(const Observable& o) { PairList pairs; const auto& idx = o.indices(); @@ -227,9 +223,8 @@ std::vector> Cuts::m_inv_min() const { } std::vector Cuts::pair_mass_any_min() const { - // The CutMode::any pair mass cuts that m_inv_min leaves out: one pair - // passing is enough, so the cut bounds no pair in particular, but it does - // bound any set of particles holding all of its pairs. + // The CutMode::any pair mass cuts that m_inv_min leaves out: they bound no + // pair in particular, but any set of particles holding all of their pairs. std::size_t n = arg_types().at(0).shape.at(0) - 2; std::vector out; for (auto& item : _cut_data) { diff --git a/madspace/src/phasespace/flow.cpp b/madspace/src/phasespace/flow.cpp index 4565c753bd..d4c1d8fa0b 100644 --- a/madspace/src/phasespace/flow.cpp +++ b/madspace/src/phasespace/flow.cpp @@ -44,7 +44,7 @@ void vegas_init( // TODO: check shapes auto bias_global = context->global(bias_name); auto grid_tensor = context->global(grid_name).cpu(); - bool is_cpu = false; // context->device() == cpu_device(); + bool is_cpu = false; Tensor bias_tensor = is_cpu ? bias_global : Tensor(DataType::dt_float, bias_global.shape()); auto bias_view = bias_tensor.view()[0]; @@ -169,9 +169,7 @@ Flow::Flow( _condition_dim(condition_dim), _bin_count(bin_count), _invert_spline(invert_spline) { - // A coupling block splits the dimensions in two non-empty halves, so a - // one-dimensional input (a 2 -> 1 phase space) would leave the flow - // without any block and nothing to train. + // a coupling block needs two non-empty halves of the dimensions if (input_dim < 2) { throw std::invalid_argument(std::format( "Flow input dimension must be at least 2, got {}", input_dim diff --git a/madspace/src/phasespace/histograms.cpp b/madspace/src/phasespace/histograms.cpp index 98d9411226..5f27f440e3 100644 --- a/madspace/src/phasespace/histograms.cpp +++ b/madspace/src/phasespace/histograms.cpp @@ -47,9 +47,7 @@ ObservableValues::ObservableValues(const std::vector& observables) : [&]() { NamedVector ret_types; for (std::size_t i = 0; i < observables.size(); ++i) { - // same type as the observable itself (event-level observables can - // return a vector per event; like the histogram op, the first - // component is histogrammed) + // event-level observables can return a vector per event ret_types.push_back( std::format("value{}", i), observables.at(i).return_types().at(0) ); diff --git a/madspace/src/phasespace/integrand.cpp b/madspace/src/phasespace/integrand.cpp index 5e2d6ac03d..fd48300451 100644 --- a/madspace/src/phasespace/integrand.cpp +++ b/madspace/src/phasespace/integrand.cpp @@ -31,18 +31,9 @@ std::size_t final_channel_count( } } -// Whether the event carries the product of its beam densities for the -// systematics to divide the PDF variations by. Not under MLM pdf reweighting: -// there the densities are taken low on the clustering ladder (pdf_scale) and -// walked back up to the factorisation scale by the ladder ratios, so the -// variations, which are evaluated at (k times) the factorisation scale, would -// be divided by densities at another scale, and even the central member would -// differ from one. Without it the systematics take the nominal densities at -// the factorisation scale themselves, and a variation becomes the ratio of -// the leading densities there, the ladder ratios left at their nominal value. // Whether the event carries the scales of its MLM alpha_s vertices, for the -// systematics to vary them the way madevent's do: the same condition under -// which mlm_weights applies them. A 2 -> 1 process has no vertex at all. +// systematics to vary them; the condition under which mlm_weights applies them. +// A 2 -> 1 process has no vertex. bool emits_alphas_scales( const std::optional& energy_scale, const std::optional& running_coupling, @@ -52,10 +43,8 @@ bool emits_alphas_scales( running_coupling && particle_count > 3; } -// Whether the event carries the MLM pdf reweighting ladder, for the -// systematics to vary it as madevent's do: every clustering scale with -// alpsfact, mu_F with the factorisation scale, and the whole ladder with each -// PDF member. +// Whether the event carries the MLM pdf reweighting ladder, for the systematics +// to vary it. bool emits_pdf_ladder( const std::optional& energy_scale, bool has_pdf, bool has_pid_options ) { @@ -70,6 +59,10 @@ const std::array ladder_slot_names{ }; const std::array ladder_beam_names{"ladder_lead_q"}; +// Whether the event carries the product of its beam densities, for the +// systematics to divide the PDF variations by. Not under MLM pdf reweighting, +// where the densities are taken at the clustering ladder's scale, not at the +// factorisation scale the variations refer to. bool emits_partial_weights( bool partial_weights, const std::optional& energy_scale ) { @@ -185,11 +178,7 @@ Integrand::Integrand( } ret_types.push_back("ren_scale", batch_float); ret_types.push_back("alpha_qcd", batch_float); - // The per-beam factorisation scales ride along with x1/x2 in - // the event record. They are worth keeping for any hadronic - // run, not just a reweighted one: a dynamical scale choice can - // give the two beams different scales, and without them the - // LHE writer has nothing to report but mu_R. + // a dynamical scale choice can give the beams different scales if (diff_xs_first.has_pdf(0)) { ret_types.push_back("x1", batch_float); ret_types.push_back("fact_scale1", batch_float); @@ -715,13 +704,9 @@ NamedVector Integrand::build_channel_part( auto scales = _energy_scale.value().build_function(fb, {momenta_acc}); std::array pdf_results; bool has_pdf_prior = false; - // The densities of beam i, for every flavour option, at the scale the - // event is generated with. Under MLM pdf reweighting the density is asked - // for low on the clustering ladder rather than at the factorisation scale, - // and walked back up by the ratios mlm_weights applies. This is madevent - // setting q2fact below q2bck before calling DSIG, and it is what the - // flavour prior should see too, since that is the density the event is - // actually generated with. + // The densities of beam i for every flavour option. Under MLM pdf + // reweighting they are taken at the bottom of the clustering ladder, not at + // the factorisation scale (mlm_weights walks them back up). auto beam_pdf = [&](std::size_t i) { auto& pdf_scale = _energy_scale->mlm_pdf_reweighting() ? scales.at(std::format("pdf_scale{}", i + 1)) @@ -744,12 +729,6 @@ NamedVector Integrand::build_channel_part( ValueVec pdf_priors; for (std::size_t i = 0; i < 2; ++i) { if (_diff_xs.at(0).has_pdf(i)) { - // Under pdf reweighting the density is asked for low on the - // clustering ladder rather than at the factorisation scale, - // and walked back up by the ratios applied further down. This - // is madevent setting q2fact below q2bck before calling DSIG, - // and it is what the flavour prior should see too, since that - // is the density the event is actually generated with. auto pdf = beam_pdf(i); pdf_results.at(i) = pdf; pdf_priors.push_back(fb.select(pdf, _pdf_indices.at(i))); @@ -845,19 +824,14 @@ NamedVector Integrand::build_channel_part( } } - // With a clustering history per diagram these scales only serve the pdf - // prior above: the history, and every weight read off it, is settled after - // the matrix element has given the diagram weights, in the common part. + // With a history per diagram these scales only serve the pdf prior; the + // history and its weights are settled in the common part. bool mlm_history_per_diagram = _energy_scale && _energy_scale->mlm_history_per_diagram(); - // The history over every diagram above was clustered before the flavour - // was known, as the flavour prior needs its scales. Some of its - // clusterings only exist for some flavours of a merged subprocess (the - // q-q' t-channel gluon of q q~ > q q~ for a q q~ > q' q~' event), so the - // event is clustered again with its own flavours, and the densities are - // taken again at the scales that gives. The prior keeps the first ones: - // it only steers the sampling. + // The first clustering ran before the flavour was known; some clusterings + // only exist for some flavours of a merged subprocess, so cluster again + // with the event's flavours. The prior keeps the first scales. if (_energy_scale && _energy_scale->is_mlm() && !mlm_history_per_diagram && !_mlm_leg_flavors.empty()) { scales = _energy_scale->build_mlm_with_flavors( @@ -881,8 +855,7 @@ NamedVector Integrand::build_channel_part( } } if (_energy_scale && _energy_scale->has_scale_veto() && !mlm_history_per_diagram) { - // Same for the floor on the scales themselves, which applies to every - // dynamical scale choice rather than only to the merging one. + // the floor on the scales applies to every dynamical scale choice weights_after_cuts.push_back(scales.at("scale_weight")); } @@ -985,21 +958,13 @@ ValueVec Integrand::mlm_weights( ) const { ValueVec weights; auto batch_size_acc = fb.batch_size({flavor_id}); - // The merging cut comes out of the clustering, which only runs for - // events that already passed the phase-space cuts, so it enters as a - // factor on the weight rather than as one of the cuts themselves. An - // event below xqcut ends up with weight zero and is never unweighted. + // The merging cut comes out of the clustering, which runs after the + // phase-space cuts, so it is a weight factor (zero below xqcut). weights.push_back(scales.at("xqcut_weight")); - // alpha_s reweighting, the CKKW-style factor madevent applies in - // Template/LO/SubProcesses/reweight.f: every clustering vertex that - // produced a parton is evaluated at its own scale rather than at the - // event's, so the weight carries prod_i alphas(pt_i) instead of - // alphas(mu_R)^n. Without it a merged sample is short by one factor - // per emission, compounding with multiplicity. - // - // The kernel hands back 0 for any vertex it does not reweight; that - // vertex is evaluated at mu_R, so its ratio is exactly one. + // alpha_s reweighting as in madevent's reweight.f: each clustering vertex is + // evaluated at its own scale, so the weight carries prod_i alphas(pt_i) / alphas(mu_R). + // A scale of 0 marks a vertex that is not reweighted (ratio one). if (_energy_scale->mlm_alphas_reweighting() && _running_coupling) { auto vertex_scales = scales.at("alphas_scales"); std::size_t vertex_count = vertex_scales.type.shape.at(0); @@ -1024,15 +989,9 @@ ValueVec Integrand::mlm_weights( } } - // pdf reweighting: the beam density, evaluated above at the bottom of - // the clustering ladder, walked back up one clustering at a time. Each - // slot the kernel marked active contributes f(x, Q_i) / f(x, Q_i-1) - // for the flavour its beam line carried at that point. - // - // The flavour is only knowable here, after the sampling: the kernel - // hands out a class - the gluon, one of the two beams' own flavours, or - // a flavour the diagram pinned down - and the table turns that plus the - // sampled option into an index into the reweighting density. + // pdf reweighting: walk the beam density back up the clustering ladder. Each + // active slot contributes f(x, Q_i) / f(x, Q_i-1) for its beam line's flavour, + // which _pdf_rw_table looks up from the kernel's flavour class and flavor_id. if (_pdf_rw && _pid_options.size() > 0) { auto flavor_classes = scales.at("pdf_rw_flavor"); std::size_t slot_count = flavor_classes.type.shape.at(0); @@ -1043,9 +1002,7 @@ ValueVec Integrand::mlm_weights( auto ones = fb.full({1., batch_size_acc}); Value factor, veto; ValueVec ladder_x, ladder_pdg, ladder_q, ladder_info; - // Slot 0 is the first clustering of the history, which can only ever - // record where a beam line's density already sits, so it never - // carries a ratio. + // slot 0 only records where a beam line's density sits, never a ratio for (std::size_t i = 1; i < slot_count; ++i) { std::vector column{static_cast(i)}; auto pick = [&](const char* name) { @@ -1053,8 +1010,8 @@ ValueVec Integrand::mlm_weights( }; auto active = pick("pdf_rw_active"); auto beam = pick("pdf_rw_beam"); - // x of the beam line: the beam's own momentum fraction times - // everything the clustering z has taken off it since. + // x of the beam line: the beam's momentum fraction times the + // clustering z taken off it so far auto x_beam = fb.add( x.at(0), fb.mul(beam, fb.sub(x.at(1), x.at(0))) @@ -1085,21 +1042,15 @@ ValueVec Integrand::mlm_weights( auto numerator = density("pdf_rw_q_num"); auto denominator = density("pdf_rw_q_den"); if (ladder) { - // what madevent books for its systematics at a step: the line's - // flavour, its momentum fraction before the step's z, and the - // scale the step takes the density to; info is active + 2 beam + // per step: flavour, x before the step's z, target scale; + // info is active + 2 * beam ladder_x.push_back(fb.mul(x_beam, pick("pdf_rw_x_prev"))); ladder_pdg.push_back(fb.gather(index, _pdf_rw_pdgs)); ladder_q.push_back(scale_of("pdf_rw_q_num")); ladder_info.push_back(fb.add(active, fb.mul(beam, 2.))); } - // madevent drops the event outright when the density it is - // dividing by falls under 1e-10, where the grid is no longer - // saying anything (reweight.f, rewgt=0 for pdfj2 < 1e-10). Same - // threshold, as a hard step: a ramp in the denominator would let - // 0 < den < 1e-10 through at num*den*1e20 instead of zero. The - // floor under the division keeps the term finite so that a - // vetoed slot multiplies to zero and not to a NaN. + // madevent vetoes the event when the denominator density is below + // 1e-10 (reweight.f); the floor keeps a vetoed term finite, not NaN auto floor = fb.full({1e-10, batch_size_acc}); // max(den, 0) is 0 for a NaN density (the comparison max is built // on picks the second argument), which cut_one alone would pass @@ -1107,8 +1058,7 @@ ValueVec Integrand::mlm_weights( fb.max(denominator, fb.full({0., batch_size_acc})), 1e-10, 1e300 ); auto ratio = fb.div(numerator, fb.max(denominator, floor)); - // An inert slot contributes exactly one, whatever its density - // came out as. + // an inactive slot contributes exactly one auto term = fb.add(ones, fb.mul(active, fb.sub(ratio, ones))); auto pass = fb.add(ones, fb.mul(active, fb.sub(usable, ones))); factor = factor ? fb.mul(factor, term) : term; @@ -1124,7 +1074,6 @@ ValueVec Integrand::mlm_weights( ladder->push_back("ladder_q", fb.stack(ladder_q)); ladder->push_back("ladder_info", fb.stack(ladder_info)); // each beam's leading density, at the scale it was evaluated at - // (clamped by apply_scale_range) ladder->push_back( "ladder_lead_q", fb.stack({scales.at("pdf_scale1"), scales.at("pdf_scale2")}) ); @@ -1170,11 +1119,6 @@ NamedVector Integrand::build_common_part( auto flavor_id = args.at("flavor_id"); auto batch_size_val = fb.batch_size({args.at("weight_before_cuts")}); - // The matrix element sees the momenta the event is written with: the mirror - // is applied inside the mapping. This matters for polarised matrix elements, - // which are not invariant under the mirror (HELAS quantises along the frame - // z axis). - // With a single flavor, flavor_id is always zero, so looking up per-flavor // tables does not need a gather on a constant tensor auto flavor_table_lookup = [&](const std::vector& table) -> Value { @@ -1228,8 +1172,8 @@ NamedVector Integrand::build_common_part( Value alpha_qcd_acc = _running_coupling.value().build_function(fb, {args.at("ren_scale")}).at(0); - // The scales and densities the event is evaluated with. They come from the - // channel part, unless the MLM history is picked per diagram further down. + // scales and densities from the channel part, unless the MLM history is + // picked per diagram below Value ren_scale_acc = args.at("ren_scale"); std::array fact_scales_acc, pdfs_acc; for (std::size_t i = 0; i < 2; ++i) { @@ -1339,16 +1283,9 @@ NamedVector Integrand::build_common_part( } } - // A clustering history per diagram. The evaluation above, at the scales of - // the history over every diagram, has supplied the diagram weights - // |A_i|^2 / sum_j |A_j|^2 (and the channel weights, which stay as they - // are). One diagram is picked from them - a choice between histories, not - // an importance sample, so it puts no factor on the weight - the event is - // clustered along it, and the cross section is taken at the scales that - // history gives: by rescaling the first evaluation when every diagram has - // the same power of alpha_s (mlm_history_qcd_power), by evaluating the - // matrix element again when coupling orders mix, as they do for gluon - // fusion and VBF in one process. + // Clustering history per diagram: pick a diagram from the diagram weights + // of the first evaluation (a choice of history, so no weight factor), + // cluster along it and take the cross section at the resulting scales. ValueVec mlm_history_weights; if (_energy_scale && _energy_scale->mlm_history_per_diagram()) { auto batch_size_acc = fb.batch_size({momenta_acc}); @@ -1392,15 +1329,10 @@ NamedVector Integrand::build_common_part( alpha_qcd_acc = _running_coupling.value().build_function(fb, {ren_scale_acc}).at(0); if (_mlm_history_qcd_power >= 0) { - // Only the scales changed, and the matrix element sees them only - // through alpha_s: with one power n of alpha_s for every diagram - // |M|^2 = C alpha_s^n, and the cross section is linear in each - // beam density. So the first evaluation, rescaled by - // (alpha_s' / alpha_s)^n f1' f2' / (f1 f2), is the second one to - // rounding - and exactly it where the history did not change, as - // for most events under clustering_history = madevent. The colour, - // helicity and diagram weights of the first evaluation stay valid: - // their probabilities are ratios of terms of the same power. + // |M|^2 ~ alpha_s^n and the cross section is linear in each beam + // density, so rescale the first evaluation by + // (alpha_s' / alpha_s)^n f1' f2' / (f1 f2). The colour, helicity + // and diagram weights stay valid (ratios of terms of the same power). auto batch_size_acc = fb.batch_size({momenta_acc}); auto ones = fb.full({1., batch_size_acc}); Value factor = ones; @@ -1409,14 +1341,12 @@ NamedVector Integrand::build_common_part( factor = fb.mul(factor, alpha_ratio); } if (_diff_xs.at(0).matrix_element_output()) { - // the stored squared matrix element is at the event's alpha_s - // too, without the beam densities + // the stored |M|^2 scales with alpha_s only, not the densities dxs_vec.back() = fb.mul(dxs_vec.back(), factor); } for (std::size_t i = 0; i < 2; ++i) { if (_diff_xs.at(0).has_pdf(i)) { - // a first density of exactly zero gave a zero weight, which - // stays zero (the guard only keeps 0 * x / 0 from a NaN) + // a zero first density stays a zero weight; the guard avoids NaN auto denominator = fb.add( pdfs_first.at(i), fb.cut_one(pdfs_first.at(i), 0., 0.) ); @@ -1426,8 +1356,7 @@ NamedVector Integrand::build_common_part( } dxs_vec.at(0) = fb.mul(dxs_vec.at(0), factor); } else { - // several powers of alpha_s mix (gluon fusion and VBF in one - // process, say): the matrix element has to be evaluated again + // mixed alpha_s powers: evaluate the matrix element again dxs_vec = _diff_xs.at(0) .build_function(fb, make_xs_args(pdfs_acc, alpha_qcd_acc)) .values(); @@ -1440,8 +1369,7 @@ NamedVector Integrand::build_common_part( ? fb.mul(diff_xs_acc, fb.gather(flavor_id, _flavor_factors)) : fb.mul(diff_xs_acc, _flavor_factors.at(0)); } - // weight_after_cuts is one by construction unless the channel part put a - // weight into it + // weight_after_cuts is one unless the channel part put a weight into it ValueVec weights_after_cuts; if (has_channel_weight_after_cuts()) { weights_after_cuts.push_back(args.at("weight_after_cuts")); @@ -1450,8 +1378,7 @@ NamedVector Integrand::build_common_part( for (auto& weight : mlm_history_weights) { weights_after_cuts.push_back(weight); } - // the bias enters the weight seen by the adaptation and the unweighting, - // the event generator divides it out again + // the bias is part of the weight; the event generator divides it out again Value bias_acc; if (_custom_bias) { ValueVec bias_args{momenta_acc}; diff --git a/madspace/src/phasespace/matrix_element.cpp b/madspace/src/phasespace/matrix_element.cpp index 174aa52fd3..eade9f0162 100644 --- a/madspace/src/phasespace/matrix_element.cpp +++ b/madspace/src/phasespace/matrix_element.cpp @@ -171,8 +171,7 @@ MatrixElement::MatrixElement( return std::vector(); } if (incoming_count == 0) { - // an empty list would build an all-zero mask, which the kernel - // reads as "no frame" and passes through + // an all-zero mask would be read by the kernel as "no frame" throw std::invalid_argument( "a process needs at least one incoming particle to define the " "frame me_frame is reached from" @@ -183,8 +182,7 @@ MatrixElement::MatrixElement( incoming.push_back(static_cast(i)); } auto mask = particle_mask(particle_count, incoming, "incoming_count"); - // asking for the rest frame of the incoming system is the reference - // frame itself: one boost gets there + // the frame is the reference frame itself: one boost suffices if (mask == _frame_mask) { mask.clear(); } @@ -268,20 +266,9 @@ NamedVector MatrixElement::build_function_impl( matrix_args.push_back(random.at(random_index)); ++random_index; } else if (input == momenta_in && !_frame_mask.empty()) { - // Evaluate the matrix element in the frame the run card asked for - // (me_frame). Only the matrix element sees the boosted momenta: - // everything else -- cuts, scales, momentum fractions, the event - // that gets written out -- keeps using the momenta as generated. - // - // madevent defines that frame as a single boost away from the rest - // frame of the incoming system, because that is the frame its - // matrix element is handed the momenta in. madspace generates them - // in the lab frame instead, so it has to go through that rest frame - // rather than boost straight there: two boosts along different - // directions do not compose into the boost between the end frames, - // and the Wigner rotation left over would rotate the polarisation - // axes (it drops out only for a Lorentz invariant matrix element, - // which is exactly the case where none of this matters). + // Only the matrix element sees the me_frame boosted momenta. Go via + // the incoming rest frame, as madevent does, since successive boosts + // along different directions leave a Wigner rotation. Value momenta = args.at(arg_index); if (!_reference_mask.empty()) { momenta = fb.boost_to_frame(momenta, _reference_mask); diff --git a/madspace/src/phasespace/mlm_clustering.cpp b/madspace/src/phasespace/mlm_clustering.cpp index edd907815d..063b3f90cc 100644 --- a/madspace/src/phasespace/mlm_clustering.cpp +++ b/madspace/src/phasespace/mlm_clustering.cpp @@ -76,8 +76,9 @@ constexpr int flavor_class_first_absolute = 3; // already been mapped through by the time it gets here. constexpr int merged_quark_representative = 1; -// Color representation of a line, as the signed color of the model (1 for a -// singlet, +-3 for a (anti)triplet, 8 for an octet, +-6 for a sextet). The +// Color representation of a line, as the absolute value of the signed color of +// the model (1 for a singlet, 3 for a (anti)triplet, 8 for an octet, 6 for a +// sextet). The // merged-flavor placeholders that survive in propagator pdg ids stand for the // particles clean_pids() maps them to in launch.py: 81 is a quark, 82 an // electron and 83 a neutrino. @@ -816,18 +817,9 @@ MLMClustering::MLMClustering( continue; } - // for the t-channel part, one of the initial state particles has to be - // involved in the clustering. find_t_vertices collects the t-channel - // propagators on the way back from beam 1 (index 0 of the incoming - // legs), so t_propagator_masses/pdg_ids start at the propagator next - // to that beam. The k-th mask accumulated from beam 1 (mask 1) is - // therefore propagator k, and the k-th from beam 2 (mask 2), walking - // the children in reverse, is propagator t_count - 1 - k. Getting - // this backwards swaps the flavour and mass of the two ends of every - // chain with two or more t-channel propagators: in g g > t t~ g the - // gluon line left after the beam emits the jet was read as the top, - // so the beam's parton line stopped at the jet and mu_R picked up - // the jet's own scale. + // t-channel lines: find_t_vertices collects the propagators from beam 1 + // on, so the k-th mask accumulated from beam 1 (mask 1) is propagator + // k, and the k-th from beam 2 (mask 2) is propagator t_count - 1 - k. auto& t_masses = topo.t_propagator_masses(); auto& t_pdg_ids = topo.t_propagator_pdg_ids(); std::size_t t_count = t_masses.size(); diff --git a/madspace/src/phasespace/mlp.cpp b/madspace/src/phasespace/mlp.cpp index 4de6a6537f..e7db21b433 100644 --- a/madspace/src/phasespace/mlp.cpp +++ b/madspace/src/phasespace/mlp.cpp @@ -87,8 +87,7 @@ void initialize_layer( auto uniform = [&](MixMaxRandom& gen) { return bound * (2. * gen.generate_double() - 1.); }; - // Independently seeded per tensor, via a fresh unique_seed_index() rather - // than a hash of the tensor's name. + // each tensor gets its own seed from unique_seed_index() MixMaxRandom weight_rand_gen( DerivedSeed(seed, DerivedSeed::global_init, context->unique_seed_index()) ); diff --git a/madspace/src/phasespace/pdf.cpp b/madspace/src/phasespace/pdf.cpp index fc3a28025c..e45f76d87b 100644 --- a/madspace/src/phasespace/pdf.cpp +++ b/madspace/src/phasespace/pdf.cpp @@ -475,7 +475,6 @@ AlphaSGrid::AlphaSGrid(const std::string& file) { throw std::runtime_error(std::format("could not open file '{}'", file)); } for (std::string line; std::getline(grid_file, line);) { - // skip comments and remove leading and trailing spaces line = trim(line); // minimalistic parser to read yaml-like data diff --git a/madspace/src/phasespace/phasespace.cpp b/madspace/src/phasespace/phasespace.cpp index 0a7e43a66f..5f777260a9 100644 --- a/madspace/src/phasespace/phasespace.cpp +++ b/madspace/src/phasespace/phasespace.cpp @@ -123,20 +123,11 @@ std::size_t ps_discrete_dim( return 0; } -// The floor every pair of final-state particles puts on its invariant mass: -// the pair mass cuts m_inv_min, raised wherever the pt and delta R cuts imply -// more. All tables are indexed by outgoing position. For two massless -// particles -// m^2 = 2 pt_i pt_j (cosh(d_eta) - cos(d_phi)), -// and on d_eta^2 + d_phi^2 >= R^2 the bracket is smallest at d_eta = 0, -// d_phi = R (moving along the circle towards d_phi = 0 costs more in cosh than -// it gains in cos, since sin b <= b), so -// m >= sqrt(2 pt_i,min pt_j,min (1 - cos R)). -// That is a consequence of the cuts, not an approximation to them, so it can -// bound the sampling without changing the integral. It needs d_phi = R to be -// reachable (R <= pi), and it needs massless legs: with a mass, delta R is -// measured in pseudorapidity while the invariant depends on the rapidity, and -// the bound no longer follows. +// Floor on the invariant mass of every pair of final-state particles: the pair +// mass cuts, raised where pt and delta R cuts imply more. Tables are indexed by +// outgoing position. For massless legs m^2 = 2 pt_i pt_j (cosh(d_eta) - +// cos(d_phi)) is smallest at d_eta = 0, d_phi = R, so m >= sqrt(2 pt_i pt_j +// (1 - cos R)). Needs R <= pi and massless legs (delta R uses pseudorapidity). std::vector> pair_mass_floors( std::vector> floors, const std::vector& pt_min, @@ -163,14 +154,10 @@ std::vector> pair_mass_floors( return floors; } -// The smallest sum_i sqrt(m_i^2 + pt_i^2) the final state can have when every -// pt_i >= pt_min_i. The beams carry no transverse momentum, so the outgoing -// transverse momenta add up to zero, and that needs the largest to be no more -// than the sum of the others. When the cuts alone leave that unsatisfied (one -// hard cut, little else cut), the others have to make up the deficit, and the -// cheapest way is to spread it over the massive ones, at equal pt_i / m_i, -// whose transverse mass grows more slowly than pt; with no massive particle -// to take it, it costs its full size. +// Smallest sum_i sqrt(m_i^2 + pt_i^2) of the final state with pt_i >= pt_min_i +// and the transverse momenta adding up to zero. If the largest pt_min exceeds +// the sum of the others, the deficit is made up by raising the massive legs at +// equal pt_i / m_i, or costs its full size if there are none. double min_transverse_mass_sum( const std::vector& masses, const std::vector& pt_min ) { @@ -191,9 +178,8 @@ double min_transverse_mass_sum( if (deficit <= 0.) { return sum; } - // raise every massive particle other than the hardest to pt = t m (where - // that is more than its cut), with t such that the raises add up to the - // deficit + // raise massive non-hardest particles to pt = t m (where above their cut), + // with t solved by bisection so the raises add up to the deficit std::vector massive; double mass_sum = 0., massive_pt_sum = 0., t_high = 0.; for (std::size_t i = 0; i < masses.size(); ++i) { @@ -327,17 +313,10 @@ PhaseSpaceMapping::PhaseSpaceMapping( }; std::vector decay_info(_topology.decays().size()); - // The cuts are indexed by the position of a particle in the event the - // mapping returns, and that event is the topology's own momenta reordered - // by the channel permutation (output[i] = topology[perm[i]], see - // permute_momenta). The same mapping serves every permutation, so a bound - // is handed to the sampler only in a form that holds for all of them: - // the weakest of the per-permutation bounds. Without permutations the - // topology order is the event order. - // - // Cuts indexes its per-particle tables by outgoing position, counting two - // incoming particles. A decay topology has one, so the tables cannot be - // read against it at all - and a decay has no cuts to apply anyway. + // Cuts are indexed by event position: the topology's momenta reordered by + // the permutation (output[i] = topology[perm[i]]). Bounds given to the + // sampler are the weakest over all permutations. Decays (one incoming + // particle) have no cuts to read. constexpr std::size_t no_index = static_cast(-1); const std::size_t n_in = _topology.incoming_masses().size(); const std::size_t n_out = _topology.outgoing_masses().size(); @@ -413,12 +392,8 @@ PhaseSpaceMapping::PhaseSpaceMapping( decay_info.at(index) = {m_min, pt_min, eta_max, std::nullopt}; } - // A cut on the invariant mass of a pair is also a statement about the - // phase space, not only about which events to keep afterwards. Wherever - // both members of the pair come out of the same propagator, the cut is a - // floor on that propagator's invariant and can be handed straight to the - // sampler, which is the difference between generating the region the cut - // allows and throwing away nearly everything generated. + // Where both members of a cut pair come out of the same propagator, the + // cut is a floor on that propagator's invariant and can go to the sampler. constexpr std::size_t no_leaf = static_cast(-1); std::vector> node_leaves(_topology.decays().size()); { @@ -445,11 +420,9 @@ PhaseSpaceMapping::PhaseSpaceMapping( std::sort(leaves.begin(), leaves.end()); } } - // A bound on the absolute rapidity of every node whose leaves all have a - // pseudorapidity cut, and negative otherwise. A leaf has |y| <= |eta|, and - // the rapidity of a sum of momenta, tanh(y) = sum_i m_T,i sinh(y_i) / - // sum_i m_T,i cosh(y_i), is a weighted mean of the tanh(y_i), so it never - // exceeds the largest |y_i|. This holds for massive particles too. + // Bound on |y| of every node whose leaves all have an eta cut (negative + // otherwise): |y| <= |eta| for a leaf, and tanh(y) of a sum is a weighted + // mean of the leaves' tanh(y_i), also for massive particles. std::vector node_y_max(node_leaves.size(), -1.); for (std::size_t d = 0; d < node_leaves.size(); ++d) { if (node_leaves.at(d).empty()) { @@ -466,33 +439,21 @@ PhaseSpaceMapping::PhaseSpaceMapping( } node_y_max.at(d) = y_max; } - // The rapidity of the partonic system, log(x1 / x2) / 2, is the root's. The - // cuts hold in the lab frame and x1, x2 live in the beams' centre-of-mass - // frame, which is boosted by beam_rapidity: |y_cm| <= |y_lab| + |y0|. (The - // bounds on the children, in the partonic frame, are unaffected.) + // The partonic rapidity log(x1 / x2) / 2 is the root's. The cuts hold in the + // lab, x1 and x2 in the beams' frame: |y_cm| <= |y_lab| + |beam_rapidity|. if (_map_luminosity && read_cuts) { double y_root = node_y_max.at(0); _y_max_lab = y_root >= 0. ? y_root + std::abs(beam_rapidity) : y_root; } - // The floor a set of final-state particles (topology outgoing positions) - // inherits from the pair mass cuts. A pair contributes at least its cut and - // every other particle at least its mass, and for future-pointing momenta - // the invariant mass of a sum is at least the sum of the invariant masses: - // * a cut every pair must pass (topo_m_inv_min) gives - // m(leaves) >= cut(i, j) + sum_{k != i, j} m_k - // for each cut pair (i, j) among the leaves; - // * a CutMode::any cut, which only one of its pairs has to pass, gives - // the smallest of those bounds over its pairs - provided every one of - // them lies among the leaves, or the passing pair may lie elsewhere. - // * every pair together: expanding (sum p_k)^2 with - // 2 p_i.p_j = m_ij^2 - m_i^2 - m_j^2 gives the exact identity - // m(leaves)^2 = sum_{i= max(cut, + // m_i + m_j). + // The any-mode cuts are read per permutation and the weakest is kept. const auto& out_masses = _topology.outgoing_masses(); auto pair_floor = [&](const std::vector& leaves) { double leaf_mass_sum = 0.; @@ -560,14 +521,8 @@ PhaseSpaceMapping::PhaseSpaceMapping( } return std::max(floor, any_floor); }; - // e_min is the propagator's own floor on its invariant mass, and - // update_mass_min_max already carries it into every s_min the sampler uses - // and into what the parents subtract, so raising it here is all that is - // needed for the cut to shape the integration. A floor at or above the top - // of an on-shell window (e_max) leaves the propagator nothing to sample: - // the cuts reject the whole channel. It is then not handed on as an - // inverted range but reported through empty(), so the channel can be - // dropped instead of failing to find a single passing point. + // Raise e_min (picked up by update_mass_min_max). A floor at or above e_max + // leaves nothing to sample and is reported through empty(). for (std::size_t d = 1; d < node_leaves.size(); ++d) { const auto& decay = _topology.decays().at(d); if (decay.child_indices.empty()) { @@ -584,38 +539,18 @@ PhaseSpaceMapping::PhaseSpaceMapping( _topology.raise_decay_e_min(d, floor); } - // The same thing one level up: a floor on the total invariant mass of the - // final state is a floor on the root propagator, which is the s-hat the - // luminosity mapping samples. Handing it over is the difference between - // sampling the region the cut allows and sampling everything and throwing - // nearly all of it away; the Invariant's Jacobian follows the range it is - // given, so the integral is unchanged and only the efficiency moves. - // - // Two sources of such a floor: a cut on sqrt(s_hat) itself, and the pair - // cuts, every one of which bounds the total exactly as it bounds a - // propagator above. + // A floor on the total final-state mass is a floor on the root, i.e. on + // s_hat. Sources: the sqrt(s_hat) cut and the pair cuts. double sqrt_s_hat_min = std::max(read_cuts ? _cuts.sqrt_s_min() : 0., pair_floor(node_leaves.at(0))); - // Only the luminosity mapping samples the root virtuality, and the root's - // e_min is read nowhere else: build_forward_impl gives the root its s_min - // only when it samples it, and the other propagators bound themselves by - // the root's mass, never by its floor. A leptonic collision has s_hat fixed - // at s_lab and chili reconstructs it from the momenta it has already - // generated, so in neither case is there a range to narrow -- the cut - // stays a filter there. A floor at or above the beam energy leaves nothing - // to sample at all, and is left to the filter too rather than handed on as - // an empty range. + // Only the luminosity mapping samples s_hat. Leptonic and chili modes have + // no range to narrow, and a floor at or above the beam energy would leave + // an empty one; the cut stays a filter in these cases. if (_map_luminosity && sqrt_s_hat_min > 0. && sqrt_s_hat_min < _sqrt_s_lab) { _topology.raise_decay_e_min(0, sqrt_s_hat_min); } - // One more floor on s_hat, from the pt cuts: in the partonic - // centre-of-mass frame sqrt(s_hat) is the sum of the outgoing energies, - // each at least the transverse mass, and the transverse momenta are the - // same there as in the lab, so - // sqrt(s_hat) >= sum_i sqrt(m_i^2 + pt_i^2), - // minimised over pt_i >= pt_i,min with the pt_i adding up to zero - // (min_transverse_mass_sum). Without a pt cut this is just the sum of the - // masses, which the sampler already respects. + // Floor on s_hat from the pt cuts: sqrt(s_hat) >= sum_i sqrt(m_i^2 + pt_i^2), + // minimised over pt_i >= pt_i,min (min_transverse_mass_sum). if (_map_luminosity && read_cuts) { bool has_pt_cut = std::any_of( topo_pt_min.begin(), topo_pt_min.end(), [](double pt) { return pt > 0.; } @@ -636,11 +571,8 @@ PhaseSpaceMapping::PhaseSpaceMapping( bool is_com_decay = decay.index == 0; if (decay.index != 0 || !has_t_channel) { if (decay.child_indices.size() == 2 && is_com_decay && read_cuts) { - // The root decays in the partonic centre-of-mass frame, which - // is the lab up to a boost along the beam, so the pt cut of a - // final-state child and the rapidity bound of any child - // restrict its polar angle (TwoBodyDecay). A composite child - // has no pt bound of its own. + // The pt cut of a final-state child and the rapidity bounds of + // the children restrict the polar angle (TwoBodyDecay). double pt_min = 0.; for (std::size_t child_index : decay.child_indices) { pt_min = std::max(pt_min, decay_info.at(child_index).pt_min); @@ -673,11 +605,8 @@ PhaseSpaceMapping::PhaseSpaceMapping( if (!is_com_decay || _map_luminosity) { double mass = decay.width == 0. ? 0. : decay.mass; double width = decay.width; - // The root is s_hat, which the PDF convolution samples. A - // color-ordered chain samples it as AmpliCol does, with - // 1/s_hat^1.2, when masses or cuts bound it from below; without a - // bound that density piles the points up at the regulator of - // Invariant (and AmpliCol falls back to a flat one). + // Color-ordered mode samples s_hat with 1/s_hat^1.2 as AmpliCol + // does, but only if it is bounded from below. double s_hat_default = t_channel_mode == PhaseSpaceMapping::color_ordered && info.m_min > 0. ? 1.2 @@ -713,10 +642,8 @@ PhaseSpaceMapping::PhaseSpaceMapping( _t_mapping = ChiliMapping(_topology.t_propagator_count() + 1, eta_max, pt_min); } else if (t_channel_mode == PhaseSpaceMapping::color_ordered) { - // Reorder the per-pair cut matrices (indexed by topology outgoing - // position) into the child order in which masses/pt are handed to the - // chain, mirroring the pt_min reordering above. Composite (non-leaf) - // children carry no pairwise cut. + // Reorder the per-pair cut matrices into the child order handed to + // the chain. Composite children carry no pairwise cut. const auto& out_idx = topology.outgoing_indices(); const auto& child_indices = topology.decays().at(0).child_indices; std::vector child_to_out( @@ -745,22 +672,17 @@ PhaseSpaceMapping::PhaseSpaceMapping( } m_inv_co.at(a).at(b) = m_inv_full.at(child_to_out.at(a)).at(child_to_out.at(b)); - // Delta R uses pseudorapidities, which a longitudinal boost - // does not shift uniformly for massive particles: a lab - // cut is no bound in the beams' frame. The cut itself is - // still applied to the lab momenta. + // Delta R is not boost invariant for massive particles, so + // a lab cut bounds nothing in the beams' frame. if (beam_rapidity == 0.) { dr_co.at(a).at(b) = dr_full.at(child_to_out.at(a)).at(child_to_out.at(b)); } } } - // The blocks of the chain that scatter the two beams (central 2->2, - // double-t, first peel of a single chain) take the rapidity bounds. - // An unset chain power is 1 with cuts, as in AmpliCol, and - // invariant_power without: then the ranges of the momentum transfers - // and s23 invariants reach zero, where 1/|x| piles the points up at - // the regulator of Invariant (AmpliCol falls back to a flat density). + // The blocks scattering the two beams (central 2->2, double-t, first + // peel of a single chain) take the rapidity bounds. The chain power + // is 1 with cuts, as in AmpliCol, to avoid ranges reaching zero. auto options = *color_ordered; auto positive = [](double x) { return x > 0.; }; bool chain_cuts = std::any_of(pt_min.begin(), pt_min.end(), positive); @@ -789,7 +711,6 @@ PhaseSpaceMapping::PhaseSpaceMapping( } } - // Random-number budget: identical computation to the declared input shape. _n_discrete = ps_discrete_dim(_topology, t_channel_mode, color_ordered); for (auto& perm : permutations) { @@ -1031,10 +952,8 @@ Mapping::Result PhaseSpaceMapping::build_forward_impl( [&](std::monostate) { auto [p1, p2] = fb.com_p_in(sqrt_s_hat); if (_topology.is_decay()) { - // Single incoming particle, at rest in the frame the decay - // products are generated in: p_in = (M, 0, 0, 0), which is - // exactly the sum of the two back-to-back beam momenta - // com_p_in builds for sqrt(s_hat) = M. + // Single incoming particle at rest: p_in = (M, 0, 0, 0) is + // the sum of the beam momenta com_p_in builds for M. p_ext = {fb.add(p1, p2)}; } else { p_ext = {p1, p2}; diff --git a/madspace/src/phasespace/scale.cpp b/madspace/src/phasespace/scale.cpp index 3f75ed9b56..9a1b894645 100644 --- a/madspace/src/phasespace/scale.cpp +++ b/madspace/src/phasespace/scale.cpp @@ -59,21 +59,10 @@ EnergyScale::EnergyScale( _max_scale(max_scale), _clustering(clustering) {} -// The range of scales an event may be evaluated at, applied to whatever -// dynamical scale choice produced them. A PDF grid only covers a band in Q - -// 1 to 10000 GeV for NNPDF23, say - and outside it the densities are not -// defined: below, a scale of a few MeV, above, one of several TeV. Either -// comes back as a NaN that no later cut can remove, because the pdf is -// evaluated before any veto weight multiplies it and NaN times zero is still -// NaN. So the scales are clamped into the range, which is what LHAPDF's -// freezing does for madevent. -// -// An MLM clustering also vetoes the event, through the scale_weight output: -// madevent's setclscales drops a merged event whose factorisation scale is -// under 2 GeV (reweight.f) rather than evaluating it at a frozen density. No -// other scale choice has that veto in madevent, so none has it here. The upper -// end has no counterpart there: LHAPDF extrapolates rather than returning a -// hole, so madevent never has to look. +// Clamp the scales into [min_scale, max_scale], the band the PDF grid covers. +// Outside it the density can be NaN, which a later veto weight cannot remove +// (NaN * 0 = NaN). An MLM clustering also vetoes events outside the range +// through scale_weight, as madevent's setclscales does for mu_F < 2 GeV. NamedVector EnergyScale::apply_scale_range( FunctionBuilder& fb, NamedVector scales ) const { @@ -85,11 +74,8 @@ NamedVector EnergyScale::apply_scale_range( Value weight; std::vector names{"fact_scale1", "fact_scale2", "ren_scale"}; if (_clustering && _clustering->pdf_reweighting()) { - // The scale the density is actually asked for under pdf reweighting. - // It sits below the factorisation scale, so clamping that one is not - // enough to keep the density inside the grid, and madevent applies its - // own 2 GeV floor to the lowered scale too rather than to the central - // one. + // Under pdf reweighting the density is evaluated at pdf_scale, which can + // lie below the factorisation scale, so it is clamped (and vetoed) too. names.push_back("pdf_scale1"); names.push_back("pdf_scale2"); } diff --git a/madspace/src/phasespace/t_propagator_mapping.cpp b/madspace/src/phasespace/t_propagator_mapping.cpp index d70168e2a0..39847ba5cd 100644 --- a/madspace/src/phasespace/t_propagator_mapping.cpp +++ b/madspace/src/phasespace/t_propagator_mapping.cpp @@ -286,7 +286,7 @@ Mapping::Result TPropagatorMapping::build_inverse_impl( } } - // ETmin per particle (see forward); Only built when cuts are active. + // ETmin per particle, as in the forward map; only used with cuts auto etmin_particle = [&](std::size_t j) { return fb.sqrt(fb.add(fb.square(m_out.at(j)), Value(pt2(j)))); }; @@ -298,7 +298,7 @@ Mapping::Result TPropagatorMapping::build_inverse_impl( } } - // sample t-invariants and build momenta of t-channel part of the diagram + // recover the t-invariant random numbers from the t-channel momenta Value k_rest = fb.add(inputs.at(0), inputs.at(1)); Value p1_rest = inputs.at(0), p2_rest = inputs.at(1); bool first = true; diff --git a/madspace/src/phasespace/three_particle.cpp b/madspace/src/phasespace/three_particle.cpp index 98b0f4ac9c..5f818c5b86 100644 --- a/madspace/src/phasespace/three_particle.cpp +++ b/madspace/src/phasespace/three_particle.cpp @@ -137,8 +137,7 @@ TwoToThreeParticleScattering::TwoToThreeParticleScattering( _p12_condition(p12_condition), _incoming_recoil(incoming_recoil), _cut_aware_branch(cut_aware_branch && has_cut) { - // The block peels a particle off a t-channel chain, where no s-channel - // resonance is expected; the arcsine sampling covers power laws only. + // The arcsine sampling covers power laws only. if (arcsine_s23 && s_width != 0.) { throw std::invalid_argument( "TwoToThreeParticleScattering: arcsine_s23 does not support a " @@ -161,10 +160,8 @@ Value negated(FunctionBuilder& fb, Value x) { return fb.sub(fb.sub(x, x), x); } std::array TwoToThreeParticleScattering::split_conditions( FunctionBuilder& fb, const NamedVector& conditions ) const { - // The kernels take the outgoing system p_12 = p1 + p2 itself. By default it - // is formed from the incoming momenta as pa + pb - p3; a caller that already - // holds p_12 more precisely (ColorOrderedMapping, where it can be a soft - // system next to the beams) passes it as the second condition instead. + // The kernels take p_12 = p1 + p2: pa + pb - p3 by default, or the second + // condition itself with p12_condition. auto p_a = conditions.at(0), p_3 = conditions.at(2); if (_p12_condition) { auto p_12 = conditions.at(1); diff --git a/madspace/src/phasespace/topology.cpp b/madspace/src/phasespace/topology.cpp index b0896cfb31..e90421399a 100644 --- a/madspace/src/phasespace/topology.cpp +++ b/madspace/src/phasespace/topology.cpp @@ -249,10 +249,8 @@ Diagram::Diagram( "Diagram must have one incoming particle (a decay) or two (a collision)" ); } - // A collision may produce a single particle (p p > h): its one-particle - // phase space is a delta function in s_hat, which PhaseSpaceMapping - // integrates against the beam momentum fractions. A decay into one - // particle has nothing to integrate at all. + // A single-particle final state is allowed for collisions only (delta + // function in s_hat), not for decays. if (outgoing_masses.size() < (incoming_masses.size() == 2 ? 1 : 2)) { throw std::invalid_argument( incoming_masses.size() == 2 @@ -311,13 +309,9 @@ std::vector Topology::topologies(const Diagram& diagram) { std::vector lines_after_t; std::vector integration_order; if (diagram.incoming_masses().size() == 1) { - // Decay: there is no t-channel chain to find. find_t_vertices walks in - // from the *second* incoming particle and marks the vertices between - // the two beams; with a single incoming particle the vertex it attaches - // to is simply the root of a pure s-channel cascade, and every other - // line at that vertex is a child of the root decay. Hand those to the - // shared decay builder below and leave _t_integration_order empty, so - // t_propagator_count() == 0 and consumers take their no-t-channel path. + // A decay has no t-channel chain: the vertex of the incoming particle + // is the root of a pure s-channel cascade, and its other lines are the + // root's children. std::size_t root_vertex = diagram.incoming_vertices().at(0); for (auto& line_ref : diagram.vertices().at(root_vertex)) { if (line_ref.type() == Diagram::incoming) { diff --git a/madspace/src/phasespace/unweighter.cpp b/madspace/src/phasespace/unweighter.cpp index 480a728c12..439bca01cb 100644 --- a/madspace/src/phasespace/unweighter.cpp +++ b/madspace/src/phasespace/unweighter.cpp @@ -31,9 +31,7 @@ BufferUnweighter::BufferUnweighter(const NamedVector& types, double quanti NamedVector BufferUnweighter::build_function_impl( FunctionBuilder& fb, const NamedVector& args ) const { - // quantile of |w|: the acceptance is on |w| (the sign is kept), so a signed - // quantile would come out too small with mixed signs, and <= 0 for a mostly - // negative integrand (an interference), accepting everything + // quantile of |w|, since the acceptance is on |w| and the sign is kept Value max_weight = fb.quantile(fb.abs(args.at(0)), _quantile); Value full_weight = args.at(0); auto [uw_indices, uw_weights] = fb.unweight(full_weight, max_weight); diff --git a/madspace/src/python/function_runtime.cpp b/madspace/src/python/function_runtime.cpp index 6f233039d5..081f449334 100644 --- a/madspace/src/python/function_runtime.cpp +++ b/madspace/src/python/function_runtime.cpp @@ -621,7 +621,7 @@ std::vector FunctionRuntime::call(std::vector args) { ProducerStream producer_stream(args); auto [inputs, runtime] = check_and_convert_args(args, *this, &_dlpack_version_cache); - // a Python matrix element called from a worker thread needs the GIL + // release the GIL so that Python matrix elements on worker threads can take it py::gil_scoped_release release; return runtime->run(inputs); } diff --git a/madspace/src/python/madspace.cpp b/madspace/src/python/madspace.cpp index 5f979924e0..8001148047 100644 --- a/madspace/src/python/madspace.cpp +++ b/madspace/src/python/madspace.cpp @@ -586,8 +586,6 @@ PYBIND11_MODULE(_madspace_py, m) { py::arg("shape"), pydoc::doc("FunctionBuilder::global") ) - //.def("instruction", &FunctionBuilder::instruction, py::arg("name"), - // py::arg("args")) .def( "product", &FunctionBuilder::product, @@ -1174,9 +1172,8 @@ PYBIND11_MODULE(_madspace_py, m) { .def_readonly("min", &Cuts::CutItem::min, pydoc::doc("Cuts::CutItem::min")) .def_readonly("max", &Cuts::CutItem::max, pydoc::doc("Cuts::CutItem::max")) .def_readonly("mode", &Cuts::CutItem::mode, pydoc::doc("Cuts::CutItem::mode")); - // Registered before the methods that return it: pybind11 writes a method's - // signature when it is defined, and a type it does not know yet ends up as - // its raw C++ name, which pybind11-stubgen rejects. + // Registered before the methods that return it, so that their signatures show + // the Python type name. py::classh(cuts, "PairMassAny", pydoc::doc("Cuts::PairMassAny")) .def_readonly( "pairs", &Cuts::PairMassAny::pairs, pydoc::doc("Cuts::PairMassAny::pairs") @@ -1602,9 +1599,8 @@ PYBIND11_MODULE(_madspace_py, m) { std::size_t>(), py::arg("matrix_element_index"), py::arg("particle_count"), - // C++ defaults to {momenta_in} / {matrix_element_out}; kept required - // in Python because pybind11-stubgen cannot render an enum-list - // default (see the header @param docs). + // C++ defaults are {momenta_in} / {matrix_element_out}; required in Python + // because pybind11-stubgen cannot render enum-list defaults. py::arg("inputs"), py::arg("outputs"), py::arg("diagram_count") = 1, diff --git a/madspace/tests/test_2to3_scattering.py b/madspace/tests/test_2to3_scattering.py index b72e39d98b..31f3544711 100644 --- a/madspace/tests/test_2to3_scattering.py +++ b/madspace/tests/test_2to3_scattering.py @@ -135,7 +135,7 @@ def input_points(rng, request): com = request.param # start with compatible incoming momenta - # COM: pa=(E,0,0,+p), pb=(E,0,0,-p) + # COM: pa=(Ea,0,0,+p), pb=(Eb,0,0,-p) pz = uniform(rng, 1000.0, 4000.0, N) ma = uniform(rng, 50.0, 300.0, N) mb = uniform(rng, 200.0, 500.0, N) @@ -273,11 +273,11 @@ def test_phase_space_compare(rng, input_points): p1, p2, det23 = mapping23.map_forward(inputs, conditions) p1s, p2s, det22 = mapping22.map_forward(inputs22, conditions22) - # det23 is now the per-branch Jacobian; the 2-solution multiplicity is owned - # externally, so apply it here to compare against the 2->2 phase-space element. + # det23 carries a factor 1/2 for the two solutions (kernel_two_to_three_particle_scattering), + # so undo it here to compare against the 2->2 phase-space element. det23 = det23 * 2.0 - # Outgoing masses must match m1, m2; spectator stays whatever it was. + # The mean Jacobians of both mappings must agree within the statistical error. std_error_23 = det23.std(correction=0).item() / math.sqrt(N) assert det23.mean().item() == approx( det22.mean().item(), abs=3 * std_error_23, rel=1e-6 @@ -298,7 +298,7 @@ def test_phase_space_volume(fixed_input_points): conditions = [fixed_input_points.pa, fixed_input_points.pb, fixed_input_points.p3] p1, p2, det = mapping23.map_forward(inputs, conditions) - # per-branch Jacobian; apply the 2-solution multiplicity externally + # undo the factor 1/2 for the two solutions contained in det det = det * 2.0 s = fixed_input_points.m12**2 diff --git a/madspace/tests/test_batch_scheduling.py b/madspace/tests/test_batch_scheduling.py index 761d3f7280..894cf1bab3 100644 --- a/madspace/tests/test_batch_scheduling.py +++ b/madspace/tests/test_batch_scheduling.py @@ -36,20 +36,16 @@ def batch_events( def test_confident_channel_hits_cap(): - # A converged channel (tiny relative error), far from its target, should - # be allowed to step close to the max_batch_fraction cap instead of - # crawling towards the target. + # A converged channel far from its target steps up to the max_batch_fraction cap. config = make_config() events = batch_events(100000, 0.0, 0, 10000, 0.001, config) - # efficiency defaults to 1 (count_requested == 0), so no raw/unweighted inflation. + # efficiency is 1 for count_requested == 0 expected = math.ceil(0.6 * 100000) assert events == expected def test_uncertain_channel_is_throttled(): - # A channel with a large cross-section relative error should get a much - # smaller batch than a converged one, since the safe lower bound on its - # target sits far below the point estimate. + # A large cross-section relative error lowers the safe target, so the batch is smaller. config = make_config() confident_events = batch_events(100000, 0.0, 0, 10000, 0.001, config) uncertain_events = batch_events(100000, 0.0, 0, 10000, 0.5, config) @@ -57,12 +53,8 @@ def test_uncertain_channel_is_throttled(): def test_no_data_is_cautious_not_full_target(): - # No cross-section data yet (pessimistic rel_error) must NOT be mistaken - # for "already done" just because the confidence bound collapses to zero - # -- it should fall through to a small exploratory request, not the full - # target in one blind shot. This is deliberately independent of the - # finish_remaining_fraction short-circuit, which only looks at how much - # is left, not at how well it's known. + # Without cross-section data the safe target is zero, which must give a small + # exploratory batch, neither "done" nor the full target. config = make_config() no_data_events = batch_events(100000, 0.0, 0, 0, 0.0, config) confident_events = batch_events(100000, 0.0, 0, 10000, 0.001, config) @@ -70,27 +62,21 @@ def test_no_data_is_cautious_not_full_target(): def test_few_events_left_finishes_outright(): - # Once the remaining deficit has shrunk below finish_remaining_fraction of - # the target, finish outright with the true deficit rather than - # continuing to throttle. + # A remaining deficit below finish_remaining_fraction of the target is requested in full. config = make_config() events = batch_events(100000, 97000.0, 0, 10000, 0.5, config) assert events == 3000 def test_mid_generation_is_meaningfully_throttled(): - # Away from both the cap regime and the finish-outright regime, the - # requested amount should still be nonzero, well below the naive full - # deficit, and shrink relative to a more certain estimate. + # Between the cap and finish-outright regimes the batch is nonzero and below the deficit. config = make_config() events = batch_events(100000, 50000.0, 0, 10000, 0.3, config) assert 0 < events < 50000 def test_always_at_least_one_event(): - # Even when the confidence bound collapses to zero (no data yet, far from - # target), the result must be strictly positive so a full exploratory - # batch still gets dispatched at the device level. + # The result is at least 1 even when the safe target is zero. config = make_config() events = batch_events(100000, 0.0, 0, 0, 0.0, config) assert events >= 1 @@ -106,9 +92,8 @@ def test_never_overshoots_true_remaining(): def test_zero_efficiency_does_not_diverge(): - # count_requested > 0 but zero observed successes must not blow up the raw event - # request (division by zero) or the caller (a real device batch) would - # never make progress against a runaway target. + # Zero unweighted events out of count_requested > 0 must not divide by zero + # or inflate the request without bound. config = make_config() events = batch_events(100000, 0.0, 5000, 10000, 0.5, config) assert events < 100000 * 5000 diff --git a/madspace/tests/test_beam_frame.py b/madspace/tests/test_beam_frame.py index 38d88c24a3..555fe0d866 100644 --- a/madspace/tests/test_beam_frame.py +++ b/madspace/tests/test_beam_frame.py @@ -132,7 +132,7 @@ def test_mirror_beams(rng, pdf_asymmetric_only): assert det1 == approx(det) assert x1_1 == approx(x1) assert x2_1 == approx(x2) - # leg 1 now moves along -z with its fraction of beam 2, leg 2 along +z + # leg 1 moves along -z with its fraction of beam 2, leg 2 along +z assert (p1[ok, 0, 3] < 0).all() assert p1[ok, 0, 0] == approx(x1[ok] * e2) assert (p1[ok, 1, 3] > 0).all() diff --git a/madspace/tests/test_buffer_unweighter.py b/madspace/tests/test_buffer_unweighter.py index 514c7b87ec..84cbe9b333 100644 --- a/madspace/tests/test_buffer_unweighter.py +++ b/madspace/tests/test_buffer_unweighter.py @@ -6,14 +6,11 @@ N / N_kept, so that training on the buffered events stays unbiased for any w_max. w_max is the quantile of |w| over the batch. -It used to be the quantile of the *signed* weights. Nothing changes for a -positive integrand, but an interference has negative weights: for a batch that -is negative throughout the signed quantile is negative and every event was kept -at its full weight (no unweighting at all), and for a mostly negative batch -with a short positive tail it came out far too small. The result stayed -unbiased (the rescaling above holds for any w_max), only the buffer filled up -with far fewer effective events. The all-negative and mixed cases below fail -with the signed quantile. +The quantile is taken over |w|, not the signed weights. A signed quantile +would be negative for an all-negative batch (every event kept, no unweighting) +and far too small for a mostly negative batch with a short positive tail; it +would stay unbiased but leave the buffer with few effective events. The +all-negative and mixed cases below guard against this. Each output event carries its input ``index``, so the per-event relations are checked exactly; the kept count and the estimator of the mean weight are @@ -94,8 +91,7 @@ def test_positive_batch(): def test_negative_batch_is_unweighted(): - """A channel where the interference is negative throughout: the signed - quantile was negative, so every event was kept at its full weight.""" + """All weights negative: a signed quantile would keep every event.""" g = generator(2) weights = -batch(g) kept = check(weights, 0.5 + rand(N, generator=g)) @@ -103,9 +99,8 @@ def test_negative_batch_is_unweighted(): def test_mixed_batch_is_unweighted(): - """85% large negative weights and a short positive tail: the signed 95% - quantile came from the positive tail, about 30 times below the |w| one, - and nearly every negative event was kept as an over-weight.""" + """85% large negative weights and a short positive tail: a signed 95% + quantile would come from the positive tail, far below the |w| one.""" g = generator(3) negative = rand(N, generator=g) < 0.85 weights = torch.where(negative, -batch(g, 10.0), batch(g, 1.0)) diff --git a/madspace/tests/test_color_ordered_amplicol_steps.py b/madspace/tests/test_color_ordered_amplicol_steps.py index 6dca3fbfaa..322a3f7bb0 100644 --- a/madspace/tests/test_color_ordered_amplicol_steps.py +++ b/madspace/tests/test_color_ordered_amplicol_steps.py @@ -9,7 +9,7 @@ - With cut_aware_branch the 2->3 steps keep the azimuthal solution that passes the pt cuts, and pick at random only when both do. - The fiducial volume agrees with RAMBO's, whatever the options. -- The function builder no longer folds 0 - x to x. +- The function builder does not fold 0 - x to x. """ import math @@ -35,8 +35,8 @@ def lsquare(p): def test_sub_of_zero_literal(): - """sub(0, x) was folded to x, which turned the bounds built from massless - literal masses upside down.""" + """sub(0, x) is -x and not x, which matters for bounds built from massless + literal masses.""" fb = ms.FunctionBuilder( ms.NamedTypes([("x", ms.batch_float)]), ms.NamedTypes([("minus_x", ms.batch_float), ("x_minus_0", ms.batch_float)]), @@ -258,7 +258,7 @@ def test_cut_volume(order, options): "order", [[0, 2, 3, 1, 4, 5], [0, 2, 3, 4, 5, 1]], ids=["2+2", "chain"] ) def test_round_trip_with_cuts(order): - """Forward and inverse agree with the new steps and the branch choice; + """Forward and inverse agree for the 2->3 steps and the branch choice; where only one solution passes, the inverse returns the one taken.""" n = 200_000 mapping = ms.ColorOrderedMapping(ms.ColorOrderedMapping.Options(order), **CUTS) diff --git a/madspace/tests/test_color_ordered_pair_cuts.py b/madspace/tests/test_color_ordered_pair_cuts.py index 9f304856ee..14abebbae3 100644 --- a/madspace/tests/test_color_ordered_pair_cuts.py +++ b/madspace/tests/test_color_ordered_pair_cuts.py @@ -3,9 +3,9 @@ In color_ordered mode PhaseSpaceMapping reorders the pairwise invariant-mass and delta-R cut matrices of Cuts (indexed by outgoing position) into the order of the t-channel children and passes them to ColorOrderedMapping, which turns -them into floors on the invariants it samples. A stray extra increment in the -loop building that reordering once skipped every odd child, so any pair cut -involving the second, fourth, ... child silently never reached the mapping. +them into floors on the invariants it samples. The tests guard against the +reordering skipping every odd child, which would silently drop any pair cut +involving the second, fourth, ... child. The process is e+ e- > d u s c: leptonic, so every random number goes straight to the t-channel mapping and PhaseSpaceMapping returns exactly the momenta of a @@ -147,14 +147,11 @@ def test_pair_cut_reaches_color_ordered_mapping( no_pair = matrix([], 0.0) p_without = co_momenta(order, pt_min, no_pair, no_pair, r, d) - # Near degenerate configurations the extra operations in - # PhaseSpaceMapping can be amplified past ATOL, so allow a few points. + # Near degenerate configurations rounding can exceed ATOL, so allow a few points. assert same_points(p_psm, p_with).double().mean() > 0.99 - # Without the comparison below the test would pass for a cut that does - # not affect this topology at all, and prove nothing. A floor only moves - # the points where it lies above the kinematic one: all of them for the - # pair masses here, about half for the delta-R cut. + # Guards against a cut that does not affect this topology at all; a floor + # only moves points where it lies above the kinematic bound. assert same_points(p_with, p_without).double().mean() < 0.9 diff --git a/madspace/tests/test_color_ordered_precision.py b/madspace/tests/test_color_ordered_precision.py index 1e28475359..e78cdff050 100644 --- a/madspace/tests/test_color_ordered_precision.py +++ b/madspace/tests/test_color_ordered_precision.py @@ -1,12 +1,9 @@ """Numerical precision of ColorOrderedMapping in long chains. -- Outgoing momenta stay on their mass shell, and still add up to the incoming - ones by construction: they used to come out off shell by up to a few 1e-5 of - their energy, from a frame that was not orthonormal and from soft momenta - formed as differences of hard ones. +- Outgoing momenta stay on their mass shell and add up to the incoming ones. - The forward/inverse round trip holds with cuts: the azimuthal frame of the - 2->2 block no longer jumps with the rounding of the incoming momentum, and - the massless branch of the block-B s23 bound no longer switches off when the + 2->2 block must not jump with the rounding of the incoming momentum, and + the massless branch of the block-B s23 bound must not switch off when the inverse reads a massless particle's mass off its momentum. """ @@ -52,7 +49,7 @@ def test_massless_on_shell(color_order): p = p[det > 0] # |p^2| / E^2 from (E - |p|) (E + |p|): ~1e-16 from rounding, up to ~1e-12 # for particles with E ~ 1e-8 sqrt(s), which the 1/sqrt(s) sampling of the - # rest masses reaches; it was up to ~2e-9 before + # rest masses reaches p_abs = torch.sqrt(torch.sum(p[..., 1:] ** 2, dim=-1)) m2 = (p[..., 0] - p_abs) * (p[..., 0] + p_abs) assert torch.max(torch.abs(m2) / p[..., 0] ** 2) < 1e-10 @@ -70,7 +67,6 @@ def test_round_trip_with_cuts(): det_inv = out[-1] ok = det > 0 rt = torch.abs(det * det_inv - 1.0)[ok] - # about 3% of the points failed before, carrying ~4% of the fiducial volume assert (rt > 1e-2).double().mean() < 1e-4 assert torch.quantile(rt, 0.999) < 1e-6 r_err = torch.max(torch.abs(r_inv - r), dim=1).values[ok] @@ -84,9 +80,8 @@ def test_round_trip_with_cuts(): ) def test_lab_frame_on_shell(masses): """The boost to the hadronic frame keeps every momentum on its mass shell - and momentum conserved: written as E cosh y + p_z sinh y, a particle moving - against the boost lost a factor e^{2|y|} of relative precision, up to - ~3e-5 in sqrt|p^2 - m^2| / E.""" + and momentum conserved. Written as E cosh y + p_z sinh y, a particle moving + against the boost would lose a factor e^{2|y|} of relative precision.""" cm_energy = 13000.0 mapping = ms.PhaseSpaceMapping( masses, @@ -105,7 +100,7 @@ def test_lab_frame_on_shell(masses): p = p[det > 1e-20] m2 = p[..., 0] ** 2 - torch.sum(p[..., 1:] ** 2, dim=-1) # |p^2 - m^2| / E^2: a few 1e-15 for massless momenta, ~1e-12 for the top - # that absorbs the rounding of the others; it reached ~1e-9 before + # that absorbs the rounding of the others off_shell = torch.abs(m2 - torch.tensor(masses) ** 2) / p[..., 0] ** 2 assert torch.max(off_shell) < 1e-11 violation = torch.abs(torch.sum(p[:, 2:], dim=1) - torch.sum(p[:, :2], dim=1)) diff --git a/madspace/tests/test_combine_channel_draw.py b/madspace/tests/test_combine_channel_draw.py index 37e4a1ad1a..f3a363b50e 100644 --- a/madspace/tests/test_combine_channel_draw.py +++ b/madspace/tests/test_combine_channel_draw.py @@ -31,16 +31,14 @@ def draw_counts(counts): def test_every_channel_gets_exactly_its_share(): - # The whole point of the draw: over the full index range each channel comes - # up exactly as often as the number of events apportioned to it. + # Over the full index range each channel is drawn as often as its count. counts = [5, 1, 12, 3, 40] assert draw_counts(counts) == counts def test_zero_share_channel_is_never_drawn(): - # A channel with no events left to give must be unreachable, wherever it - # sits -- including index 0, which is the case a lower-bound draw gets - # wrong, and which is reached by every draw once the total is down to one. + # A channel with a zero share is unreachable, including at index 0, which a + # lower-bound draw would hand to it. counts = [0, 7, 0, 0, 4, 0] assert draw_counts(counts) == counts diff --git a/madspace/tests/test_cut_kernels.py b/madspace/tests/test_cut_kernels.py index b605cd9902..708c4da8e4 100644 --- a/madspace/tests/test_cut_kernels.py +++ b/madspace/tests/test_cut_kernels.py @@ -2,13 +2,12 @@ cut_all / cut_any apply one window [min, max] (two scalars, as the instruction set declares them) to every observable of a vector: all of them inside, or at -least one inside. The kernels used to take the bounds as FIn, so the -runtime built a 2-dimensional view of these 1-dimensional tensors from shape -and stride entries that were never set, and min[i] read out of bounds: a memory -fault on HIP (MI300A, any process with a cut on several jets), silently min[0] -on the CPU and CUDA builds. The second test guards every kernel against that -mismatch: no kernel input may have more inner dimensions than its instruction -declares. +least one inside. A kernel that took the bounds as FIn would make the +runtime build a 2-dimensional view of a 1-dimensional tensor from unset shape +and stride entries, so min[i] would read out of bounds (a memory fault on HIP, +silently min[0] on CPU and CUDA). The second test guards every kernel against +that mismatch: no kernel input may have more inner dimensions than its +instruction declares. """ import glob diff --git a/madspace/tests/test_cut_rapidity_windows.py b/madspace/tests/test_cut_rapidity_windows.py index 5c7c572a50..ace0b96ffe 100644 --- a/madspace/tests/test_cut_rapidity_windows.py +++ b/madspace/tests/test_cut_rapidity_windows.py @@ -191,8 +191,7 @@ def test_dy_every_generated_point_passes(): def test_dy_windows_are_reached(): """The bounds sampled are the cuts themselves, not something stricter.""" - # every sampled point, not only those with a weight: the weight already - # contains the cut mask + # all sampled points, as the weight already contains the cut mask _, p_ext, x1, x2, _ = sample( ms.PhaseSpaceMapping(s_channel_two_body(), CM_ENERGY, cuts=dy_cuts()) ) @@ -290,7 +289,7 @@ def test_composite_rapidity_windows(): c = eegg_cuts() topology = s_channel_four_body() _, p_ext, x1, x2, det = sample(ms.PhaseSpaceMapping(topology, CM_ENERGY, cuts=c)) - # every sampled point: the weight already contains the cut mask + # all sampled points, as the weight already contains the cut mask keep = finite(p_ext) eta_max = max(ETA_LEPTON, ETA_JET) assert torch.abs(beam_rapidity(x1, x2)[keep]).max() <= eta_max + 1e-9 @@ -334,7 +333,7 @@ def test_no_rapidity_window_with_an_uncut_particle(): def test_no_cuts_no_change(): - """Without cuts the mapping is the one it always was.""" + """Empty cuts give the same mapping as no cuts.""" topology = s_channel_two_body() a = sample(ms.PhaseSpaceMapping(topology, CM_ENERGY)) b = sample(ms.PhaseSpaceMapping(topology, CM_ENERGY, cuts=ms.Cuts(4))) @@ -441,9 +440,9 @@ def photon_cuts(pids, eta=ETA_PHOTON): def test_t_channel_every_generated_point_passes(): - """u u~ > a a through the t channel: with the pt cut (already used for |t|) - and now the photons' eta cuts in |t| and in Y, every sampled point - passes, while without the cuts handed over many do not.""" + """u u~ > a a through the t channel: with the pt cut and the photons' eta + cuts bounding |t| and Y, every sampled point passes, while without the + cuts handed over many do not.""" c = ms.Cuts(photon_cuts(AA_PIDS)) topology = t_channel_two_body() _, p_free, _, _, _ = sample(ms.PhaseSpaceMapping(topology, CM_ENERGY)) diff --git a/madspace/tests/test_cuts.py b/madspace/tests/test_cuts.py index f9d278ccf6..042d918870 100644 --- a/madspace/tests/test_cuts.py +++ b/madspace/tests/test_cuts.py @@ -1,3 +1,6 @@ +"""Cut volume of PhaseSpaceMapping modes (with and without the cuts handed to the +mapping) against RAMBO with the cuts applied afterwards.""" + import math from dataclasses import dataclass @@ -7,9 +10,6 @@ import madspace as ms -# from typing import List, Optional - - PSM = ms.PhaseSpaceMapping CM_ENERGY = 13000.0 @@ -41,10 +41,6 @@ class Config: piped: bool -# def _mode_name(mode): -# return str(mode).rsplit(".", 1)[-1] - - def _n_jets(pids_out): return sum(1 for pid in pids_out if pid == 21) diff --git a/madspace/tests/test_discrete_sampler.py b/madspace/tests/test_discrete_sampler.py index d8b29508a2..23fcdfaa8a 100644 --- a/madspace/tests/test_discrete_sampler.py +++ b/madspace/tests/test_discrete_sampler.py @@ -7,8 +7,7 @@ that by construction instead of via a sentinel: an option with probability zero can never be selected, and if every option in a row is zero there is no real choice to make, so the sampler deterministically returns index 0 with -probability 1 (det == 1), same as any other certain draw. Callers no longer -need to special-case a "degenerate" result. +probability 1 (det == 1), same as any other certain draw. """ import torch @@ -68,11 +67,8 @@ def run(builder_fn, output_names, inputs, output_types=None): def test_forward_det_is_always_at_least_one(): - """det is 1/p, so a det that came from an actual choice is >= 1. The - all-zero row is not an exception to that anymore: it deterministically - gets index 0 and probability 1, so det == 1 there too, and callers no - longer need to split det into a separate density factor and weight mask - the way build_channel_part used to. + """det is 1/p, so it is >= 1. The all-zero row is no exception: it gets + index 0 and probability 1, so det == 1 there too. """ def build(fb, sampler): @@ -92,9 +88,8 @@ def build(fb, sampler): def test_forward_selects_the_expected_bucket(): - """Basic correctness of the inverse-CDF walk: r selects the option whose - cumulative-probability bucket contains it. For [0.3, 0.2, 0.4, 0.1] the - bucket edges are [0.3, 0.5, 0.9, 1.0]. + """r selects the option whose cumulative-probability bucket contains it. + For [0.3, 0.2, 0.4, 0.1] the bucket edges are [0.3, 0.5, 0.9, 1.0]. """ prior = torch.tensor([0.3, 0.2, 0.4, 0.1]).repeat(5, 1) random = torch.tensor([0.0, 0.29, 0.31, 0.89, 0.99]) @@ -113,10 +108,8 @@ def build(fb, sampler): def test_forward_never_selects_a_zero_probability_option(): - """Only one option carries any density; every draw of r must land on it. - The zero-probability options are masked out of the cumulative walk - directly, rather than relying on r never landing exactly on their - zero-width buckets. + """Only one option carries any density; every draw of r must land on it, + including r = 0, which sits on the edge of the zero-width buckets. """ prior = torch.tensor([0.0, 0.0, 0.0, 0.7]).repeat(5, 1) random = torch.tensor([0.0, 0.25, 0.5, 0.75, 0.999999]) @@ -135,9 +128,8 @@ def build(fb, sampler): def test_forward_and_inverse_dets_are_reciprocal(): """build_forward's det for the option it draws is 1/p; build_inverse's - det for that same option is p. The two must multiply to 1, the usual - Mapping invariant for a forward/inverse pair -- including on the - all-zero row, where both sides collapse to the certain-draw det of 1. + det for that same option is p, so the product is 1, also on the all-zero + row where both dets are 1. """ def build(fb, sampler): diff --git a/madspace/tests/test_doc_convention.py b/madspace/tests/test_doc_convention.py index 6b3b0b7dc1..22d8e5c689 100644 --- a/madspace/tests/test_doc_convention.py +++ b/madspace/tests/test_doc_convention.py @@ -2,7 +2,7 @@ For each documented ``Mapping`` subclass under ``phasespace/``, construct it with every parameter its docstring names passed **by keyword**. This fails if a -binding drops a constructor argument (as ``DoubleT`` did with ``has_cut``) or +binding drops a constructor argument (e.g. ``has_cut`` of ``DoubleT``) or renames one away from the header, keeping the rendered Parameters list honest. """ @@ -136,7 +136,7 @@ def test_phasespace_mapping_both_constructors(): def test_matrix_element_index_is_not_diagram_count(): - """`MatrixElement.matrix_element_index` was bound to `diagram_count`.""" + """`matrix_element_index` and `diagram_count` are separate parameters.""" me = ms.MatrixElement( matrix_element_index=3, particle_count=4, diff --git a/madspace/tests/test_double_t.py b/madspace/tests/test_double_t.py index 35de0fbbe7..52fbe1af48 100644 --- a/madspace/tests/test_double_t.py +++ b/madspace/tests/test_double_t.py @@ -87,12 +87,12 @@ def input_points(rng, request): pa, pb = beams(pz) # massless beams in COM if not com: - # boost to a random lab frame (the same recipe used in the 2->3 test) + # lab frame: beams with smeared momenta pa, pb = lab_beams(rng) # m1: the fixed single-particle mass # mir_min: lower bound on recoil mass (e.g. sum of constituent masses). - # Pick mir_min well below the sqrt(s) so there\'s room for the recoil. + # Pick mir_min well below the sqrt(s) so there's room for the recoil. m1 = uniform(rng, 1.0, 50.0, N) mir_min = uniform(rng, 50.0, 200.0, N) @@ -219,7 +219,7 @@ def test_inverse(input_points): def test_phase_space_volume_analytic(fixed_input_points): - """The integral of DoubleT\'s det over (r_phi, r_t1, r_t2) should equal + """The integral of DoubleT's det over (r_phi, r_t1, r_t2) should equal the 2-body phase-space volume integrated over m_ir^2 in [mir_min^2, (sqrt(s)-m1)^2]: @@ -279,14 +279,9 @@ def integrand(mir_sq): def test_inverse_lab_specifically(rng): - """Explicit LAB test: round-trip should work in any frame, not just COM. - - This was the failing case for the 2->3 mapping before we fixed the - rotation; keep it as a guard for double_t too. - """ + """The round trip must work in a lab frame, not just the COM frame.""" mapping = ms.DoubleT() - # boosted lab frame pz = uniform(rng, 1000.0, 4000.0, N) pa, pb = lab_beams(rng) @@ -309,7 +304,7 @@ def test_inverse_lab_specifically(rng): # ---------------------------- -# Massless m1 stress tests (regression for the t-bound precision issue) +# Massless m1 stress tests (precision of the t bounds) # ---------------------------- @@ -324,8 +319,8 @@ def test_inverse_lab_specifically(rng): ) def massless_input_points(rng, request): """Near-massless m1 stresses the inverse: lsquare(p1) suffers - catastrophic cancellation when m1 is recovered from it. With m1 as a - condition (Option B fix), this should round-trip cleanly.""" + catastrophic cancellation when m1 is recovered from it, so m1 is taken + from the conditions instead.""" _, m1_val = request.param pa, pb = beams(torch.full((N,), 6500.0)) # 13 TeV @@ -362,8 +357,7 @@ def test_inverse_massless(massless_input_points): # det round-trip assert_approx(inv_det, 1 / det, rel=1e-5) - # random round-trip; r_t2 used to be the most sensitive (its bounds - # depend on t1_abs which depended on the lossy m1^2 recompute). + # random round-trip; r_t2 is the most sensitive, as its bounds depend on t1_abs for i, (inp, inv_inp) in enumerate(zip(inputs, inv_inputs)): assert_approx( inp, inv_inp, rel=1e-5, abs=1e-8, msg=f"mismatch in input index {i}" @@ -371,7 +365,7 @@ def test_inverse_massless(massless_input_points): def test_phase_space_volume_massless(rng): - """Volume integral should still match analytic for massless m1.""" + """The volume integral matches the analytic one for massless m1.""" from scipy.integrate import quad m1_val = 0.0 diff --git a/madspace/tests/test_flow.py b/madspace/tests/test_flow.py index 6ab45c8957..c5c03916e3 100644 --- a/madspace/tests/test_flow.py +++ b/madspace/tests/test_flow.py @@ -50,9 +50,8 @@ def test_block_gradient(inverse): def test_one_dimensional_flow_is_refused(): - # a coupling block needs two non-empty halves: a 2 -> 1 phase space has a - # single random number, which used to leave a flow without any block and - # crash initialize_from_vegas with an IndexError + # a coupling block needs two non-empty halves, so a flow over a single random + # number (2 -> 1 phase space) would have no block at all for input_dim in [0, 1]: with pytest.raises(ValueError, match="at least 2"): ms.Flow(input_dim=input_dim) diff --git a/madspace/tests/test_format.py b/madspace/tests/test_format.py index 5cfa79be4c..28538368fc 100644 --- a/madspace/tests/test_format.py +++ b/madspace/tests/test_format.py @@ -1,8 +1,6 @@ """Unit tests for the string-formatting helpers in format.cpp/format.hpp, exposed to Python for testing (format_si_prefix, format_with_error, -format_progress). None of these had direct unit tests before; they were only -exercised indirectly through the pretty/log console output of a full -event-generation run. +format_progress). """ import madspace as ms diff --git a/madspace/tests/test_histogram_op.py b/madspace/tests/test_histogram_op.py index f186cd91f4..5ada444f7e 100644 --- a/madspace/tests/test_histogram_op.py +++ b/madspace/tests/test_histogram_op.py @@ -1,10 +1,8 @@ """The histogram instruction: n_bins interior bins plus an underflow and an overflow bin. Its outputs hold n_bins + 2 values, in that order: underflow, the interior bins, overflow. -The GPU runtime reduces the events to these n_bins + 2 keys (and used to do so into a -scratch buffer of n_bins, summing the squared weights of the wrong events in each bin); -the GPU CI runs these tests on a GPU via MADSPACE_TEST_DEVICE, where the large histogram -is compared with the CPU runtime. +The GPU runtime reduces the events to these n_bins + 2 keys. With MADSPACE_TEST_DEVICE +set to a GPU, the large histogram is compared with the CPU runtime. """ import torch diff --git a/madspace/tests/test_lhe.py b/madspace/tests/test_lhe.py index 63eb3ba93b..fc7deb97ee 100644 --- a/madspace/tests/test_lhe.py +++ b/madspace/tests/test_lhe.py @@ -13,8 +13,7 @@ HERE = os.path.dirname(os.path.realpath(__file__)) # Copy of PROC_tt_decay/SubProcesses/subprocesses.json (a hadronically decaying -# ttbar process), checked in here since the PROC_tt_decay output directory -# itself isn't guaranteed to exist/persist. +# ttbar process). TT_DECAY_SUBPROCESSES = os.path.join( HERE, "test_data", "lhe", "tt_decay_subprocesses.json" ) @@ -168,7 +167,7 @@ def test_event_attributes_are_bound_to_their_own_members(): Guards against copy-paste slips in the pybind11 binding block: a def_readwrite pointing at the wrong struct member silently shadows another - field (alpha_qcd used to be bound to LHEEvent::process_id). + field (e.g. alpha_qcd bound to LHEEvent::process_id). """ values = dict( process_id=7, @@ -192,7 +191,7 @@ def test_event_attributes_are_bound_to_their_own_members(): def test_event_header_line_written_to_lhe(): """The event header line of the written LHE file carries the values set - from Python, in the order defined by arXiv:0109068 (NUP IDPRUP XWGTUP + from Python, in the order defined by hep-ph/0109068 (NUP IDPRUP XWGTUP SCALUP AQEDUP AQCDUP). The attributes are set one by one rather than passed to the constructor, so @@ -363,10 +362,9 @@ def test_resonance_colors_consistent_with_daughters(events, subproc_meta): def test_external_color_flows_match_input_across_subprocesses( topology_and_args, mapping, subproc_meta ): - """Regression test for a color-offset bug where a second subprocess's - external colors were read from the wrong location in the flattened color - table. Uses two (identical) subprocesses so the offset for subproc index 1 - is actually exercised. + """Colors of a later subprocess must be read from its own part of the + flattened color table. Uses two (identical) subprocesses so that the offset + for subproc index 1 is exercised. """ _, _, subproc_args = topology_and_args lhe_completer = ms.LHECompleter([subproc_args, subproc_args], bw_cutoff=BW_CUTOFF) @@ -483,7 +481,7 @@ def test_invalid_color_index_raises(lhe_completer, mapping): def test_particle_momentum_kwargs_match_attributes(): """The constructor kwargs for the momentum components are spelled like the attributes they set: px/py/pz, matching LHEParticle's C++ members and the - rest of madspace. The old p_x/p_y/p_z spelling is gone.""" + rest of madspace; p_x/p_y/p_z are rejected.""" particle = ms.LHEParticle(px=1.5, py=-2.5, pz=3.5, energy=4.5) assert (particle.px, particle.py, particle.pz) == (1.5, -2.5, 3.5) assert particle.energy == 4.5 diff --git a/madspace/tests/test_madnis_adaptive_prob.py b/madspace/tests/test_madnis_adaptive_prob.py index 5dcb79e7d1..6047b72a81 100644 --- a/madspace/tests/test_madnis_adaptive_prob.py +++ b/madspace/tests/test_madnis_adaptive_prob.py @@ -2,13 +2,12 @@ density the event was sampled with, q, for both settings of ``drop_cuts_and_rescale``. -The MadNIS loss and the replay-buffer unweighter take it as q. It is the product of -the Jacobians of the adaptive mappings, 1/q, inverted: with -``drop_cuts_and_rescale`` the inversion came with the rescaling by N / N_acc, but -without it the Jacobian was returned as it is, so that training with -``drop_zero_integrands = false`` used 1/q instead of q. Checked against the -density from IntegrandProbability at the same points, with a randomized flow so -that q is not constant. +The MadNIS loss and the replay-buffer unweighter take it as q. The product of the +Jacobians of the adaptive mappings is 1/q, so it has to be inverted for both +settings (with ``drop_cuts_and_rescale`` also rescaled by N / N_acc); returning +the Jacobian itself would make training with ``drop_zero_integrands = false`` +use 1/q. Checked against the density from IntegrandProbability at the same +points, with a randomized flow so that q is not constant. """ import pytest diff --git a/madspace/tests/test_me_frame.py b/madspace/tests/test_me_frame.py index 205c75b8e2..e2bc519383 100644 --- a/madspace/tests/test_me_frame.py +++ b/madspace/tests/test_me_frame.py @@ -37,9 +37,7 @@ def momenta(): ) # last particle balances the rest, its mass is whatever comes out p[:, N_PARTICLES - 1] = p[:, 0] + p[:, 1] - p[:, 2:-1].sum(dim=1) - # the masses above leave only ~9 GeV of headroom at the low end of - # e1 + e2, so say so rather than let a changed seed or mass look like a - # kernel bug + # little headroom at low e1 + e2: fail here, not as a kernel bug assert (p[:, N_PARTICLES - 1, 0] > 0).all(), "balancing leg has no energy" total = p[:, :2].sum(dim=1) assert (total[:, 0] ** 2 - (total[:, 1:] ** 2).sum(dim=1) > 0).all() @@ -226,8 +224,7 @@ def matrix_element_graph(me_frame, incoming_count=2): def test_matrix_element_emits_the_boost(): - """The masks are only half the story: what matters is how many - boost_to_frame calls end up in front of the matrix element.""" + """The number of boost_to_frame calls in front of the matrix element.""" # no frame asked for: the momenta go in untouched assert matrix_element_graph([]).count("boost_to_frame") == 0 # the rest frame of the incoming system is one boost away from the lab diff --git a/madspace/tests/test_mlm.py b/madspace/tests/test_mlm.py index 2e1f2fb366..90d3830adf 100644 --- a/madspace/tests/test_mlm.py +++ b/madspace/tests/test_mlm.py @@ -6,10 +6,9 @@ machine for one phase-space point, picks a history, and reads the renormalisation / factorisation / per-jet scales off it. -There is no single "right answer" to compare against (see the discussion in the -branch notes: the resonance handling and the factorisation-scale definition -differ from what madevent does on purpose), so these tests pin down the -properties the implementation must have whatever those choices end up being: +There is no single "right answer" to compare against (the resonance handling and +the factorisation-scale definition differ from madevent's on purpose), so these +tests pin down properties the implementation must have: * the compiled state machine is well formed and reachable, * the kernel's outputs are finite, positive and ordered as documented, @@ -119,13 +118,8 @@ def run(clustering, momenta): def run_all(clustering, momenta): - """As run(), plus the trailing xqcut_weight. - - Deliberately stops there: the clustering also returns the per-vertex - alpha_s scales and their weight, which these tests are not about, and - unpacking everything would break every caller each time an output is - added. Use alphas_outputs() for those. - """ + """As run(), plus the xqcut_weight. The alpha_s and pdf outputs that follow + are left out; see alphas_outputs() and pdf_outputs().""" return tuple(np.asarray(v) for v in clustering(momenta))[:6] @@ -150,10 +144,9 @@ def alphas_outputs(clustering, momenta): def pdf_outputs(clustering, momenta): - """The pdf reweighting outputs, by name: the scale each beam density is to - be evaluated at, and one slot per step of the ladder it is walked back up. - Named rather than positional because there are eight of them and their - order is not something a test should be pinning down.""" + """The pdf reweighting outputs (the trailing outputs of the clustering), by + name: the scale each beam density is to be evaluated at, and one slot per + step of the ladder it is walked back up.""" out = clustering(momenta) first = len(out) - len(PDF_OUTPUTS) return { @@ -412,8 +405,8 @@ def test_scales_are_azimuthally_invariant(process): "clustering_mean_pdf_ladder": {"clustering_mean_pdf_ladder": True}, } -# What the beam-line walk hands the pdf reweighting, all of it built from -# transverse masses and invariants. +# What the beam-line walk hands the pdf reweighting (built from transverse +# masses and invariants). LADDER_OUTPUTS = ["pdf_scale1", "pdf_scale2", "pdf_rw_x", "pdf_rw_q_num", "pdf_rw_q_den"] @@ -434,10 +427,9 @@ def test_beam_line_scales_are_azimuthally_invariant(process, scheme): Equal measures are common there. With two massless partons left in the final state they balance in transverse momentum, so clustering either one - into the beam it travels along scores the same. Settled by rounding, a - rotation moved mu_F from 832 to 1195 GeV for one ttgg event in 200; the - tie now goes the way cluster.f's loop order takes it, so nothing may - change, not even which leg a jet scale is booked against. + into the beam it travels along scores the same. Ties are broken in the + order of cluster.f's loops rather than by rounding, so nothing may change + under a rotation, not even which leg a jet scale is booked against. """ _, diagrams, pdg_ids = process clustering = make_clustering( @@ -777,7 +769,7 @@ def pt_eta_phi(pt, eta, phi): def test_a_nearly_collinear_pair_keeps_its_clustering_scale(hadronic): """The clustering scale of two partons 1e-5 apart. Written as in cluster.f, cosh(deta) - cos(dphi) (or 1 - cos_theta) is the difference of two numbers - within 1e-10 of one, which left about 1e-6 of relative error, enough for + within 1e-10 of one, which loses about 1e-6 of relative error, enough for scalar and SIMD math to give different scales and histories. The kernel's form of the measure has none of that cancellation.""" jet_radius = 0.4 @@ -879,9 +871,9 @@ def test_single_clustering_scale_matches_the_measures(): def test_the_default_jet_radius_is_madevents(): """madevent's hidden run_card d (common/to_dj/D in kin_functions.f) and - FxFx both put 1 in the final-state kt measure. With 0.4 that measure came - out 2.5 times larger, and the merging cut weaker for final-state - splittings, than in the madevent samples it is compared against.""" + FxFx both put 1 in the final-state kt measure. A radius of 0.4 would make + that measure 2.5 times larger, and the merging cut weaker for final-state + splittings, than in madevent.""" diagrams = load_diagrams(PROCESSES["ttggg"][0]) pdg_ids = PROCESSES["ttggg"][1] momenta = sample_momenta(diagrams) @@ -950,8 +942,8 @@ def test_kt_measure_matches_the_fortran(): # -------------------------------------------------------------------------- # # The kernel walks a flat integer array. A non-terminal state is a run of -# (data, next_offset) pairs ending at the pair whose data has bit 30 set; a -# terminal state is a count followed by that many diagram indices. Which of the +# (data, next_offset, trace_data) triples ending at the triple whose data has +# bit 30 set; a terminal state is a count followed by that many diagram indices. Which of the # two a given offset is depends only on how many clusterings have been done, so # the walker below tracks the depth the same way the kernel does. @@ -1262,9 +1254,8 @@ def test_extreme_kinematics_do_not_break_the_walk(process): it would jump to a stale or unset state offset and read outside the state machine. - Strongly asymmetric beam energies reach that configuration, so this is a - regression test for the crash it used to cause. Three guards stand between - that crash and this assertion - dj_clus zeroing a NaN, the selection + Strongly asymmetric beam energies reach that configuration. Three guards + stand between that crash and this assertion - dj_clus zeroing a NaN, the selection mapping a non-finite measure onto SCALE_MAX, and taking the first candidate unconditionally - and they are deliberately redundant, so dropping any one of them leaves the others covering it. What is pinned here is the behaviour @@ -1993,8 +1984,7 @@ def test_a_density_too_small_to_divide_by_drops_the_event(tmp_path, scheme): 1e-10. With x f = 1e-12 everywhere every step of the ladder is such a division, so every event with a step on it must come out at zero. - A ramp in the denominator used to let these through at num den 1e20 - a - hundredth of the weight per step here, not zero.""" + A smooth ramp in the denominator would leave a small nonzero weight.""" weights, momenta = t_channel_integrand_weights(tmp_path, 1e-12, **scheme) clustering = {k: v for k, v in scheme.items() if k != "min_scale"} out = pdf_outputs(t_channel_clustering(pdf_reweighting=True, **clustering), momenta) @@ -2011,8 +2001,8 @@ def test_no_pdf_product_for_the_systematics_under_pdf_reweighting(tmp_path): """The systematics divide each PDF variation, taken at (k times) the factorisation scale, by the event's partial_weight_product. Under pdf reweighting that product is of the densities at pdf_scale, low on the - clustering ladder, so even the central member came out != 1. The - integrand leaves it out there, and the systematics take the nominal + clustering ladder, so even the central member would not give a ratio of 1. + The integrand leaves it out there, and the systematics take the nominal densities at the factorisation scale themselves.""" def output_count(**kwargs): return len(t_channel_integrand_outputs(tmp_path, 1.0, 16, **kwargs)) @@ -2050,8 +2040,8 @@ def w_plus_jet_diagram(): That vertex is not a QCD splitting - the W carries no color - and madevent never applies the merging cut to a leg that was not emitted at a jet vertex - (its iqjets gate). Reading the cut off any vertex instead removed 19% of - this process at xqcut = 60, all of it jets that were already above the cut. + (its iqjets gate). Reading the cut off any vertex would veto jets that are + already above the cut. """ return [ { @@ -2197,9 +2187,7 @@ def transverse_mass(p): def test_an_s_channel_qcd_root_uses_the_mean_transverse_mass_of_the_pair(): """q q~ > g > t t~ is reported at sqrt(mT(t) mT(t~)), madevent's mt2last, - for both scales. Before this rule the root kept the mT of the whole final - state, sqrt(shat), which made q q~ > t t~ 18% low against madevent at - 0 jets.""" + for both scales, rather than at the mT of the whole final state, sqrt(shat).""" gluon = (0.0, 0.0, 21) momenta = sample_momenta(s_channel_ttbar_diagram(gluon), batch_size=500) ren, fac1, fac2, _, _ = run(s_channel_ttbar_clustering(gluon), momenta) @@ -2298,9 +2286,9 @@ def first_initial_state_transitions(clustering, n_ext): def test_each_end_of_a_t_channel_chain_gets_its_own_flavour(): """Beam 0 emitting the jet leaves the gluon propagator, massless and a jet; beam 1 taking back the t~ leaves the top propagator, massive and not a jet. - With the two ends swapped the gluon was read as the top, the beam's parton - line stopped at the jet, and mu_R picked up the jet's own scale - 3 to 12% - too much t t~ + jets against madevent.""" + With the two ends swapped the gluon would be read as the top, the beam's + parton line would stop at the jet, and mu_R would pick up the jet's own + scale.""" first = first_initial_state_transitions(gg_ttbar_g_chain_clustering(), 5) assert first[(0, 4)] == (0, 1), "beam 0 + jet should leave a massless jet line" assert first[(1, 3)] == (1, 0), "beam 1 + t~ should leave a massive, non-jet line" @@ -2310,11 +2298,10 @@ def test_a_jet_emitted_before_the_tops_does_not_set_the_renormalisation_scale(): """setclscales carries a beam's parton line on past a jet emission to the vertex where the top attaches, so when the jet is clustered into a beam and the tops make up the rest, all four scales in the mu_R average are central - ones and mu_R equals mu_F. The jet's own scale entering mu_R is exactly the - symptom of the swapped chain, whose kernel gave mu_R == mu_F on no point. - It also needs the root measured where cluster.f measures it: not boosted - after the last clustering, and in the current frame after an initial-state - one. Taking it in the lab left mu_R a few per cent below mu_F.""" + ones and mu_R equals mu_F. The jet's own scale entering mu_R would break + that. It also needs the root measured where cluster.f measures it: not + boosted after the last clustering, and in the current frame after an + initial-state one; measured in the lab, mu_R would sit below mu_F.""" clustering = gg_ttbar_g_chain_clustering() momenta = sample_momenta(gg_ttbar_g_chain_diagram(), batch_size=4000) ren, fac1, fac2, outgoing, _ = run(clustering, momenta) @@ -2418,8 +2405,7 @@ def test_the_madevent_measure_takes_the_jet_into_the_beam(): def test_the_fxfx_measure_lets_the_jet_pair_with_the_w(): """The FxFx score of the (W, d) pair is often below the d's pt, so some jets - pair with the W instead - which is exactly what made W + 1 jet (quark jets) - and h + 1 jet (gluon jets) low against madevent.""" + pair with the W instead.""" momenta = sample_momenta(w_plus_jet_two_diagrams(), batch_size=4000) fxfx = jet_into_beam_fraction( w_plus_jet_two_diagram_clustering( @@ -2627,8 +2613,8 @@ def test_a_quark_that_is_not_a_merging_jet_is_written_at_the_collider_energy(): def test_the_emission_scheme_still_books_the_radiating_quark(): - """The emission scheme is not madevent's and keeps booking the (u, g) - vertex onto both of its legs.""" + """The emission scheme books the (u, g) vertex onto both of its legs, + unlike madevent's.""" emission = fsr_jet_scales(jet_scale_scheme=ms.MLMClustering.JetScaleScheme.emission) np.testing.assert_array_equal(emission[:, 0], emission[:, 2]) np.testing.assert_array_equal(emission[:, 1], CM_ENERGY) @@ -2750,8 +2736,9 @@ def two_to_one_momenta(batch_size=BATCH_SIZE, seed=SEED): list(ms.MLMClustering.ClusteringHistory.__members__.values()), ) def test_a_two_to_one_process_takes_its_scales_from_shat(scale_scheme, history): - """Used to throw "MLM clustering found no valid clustering for this - process", which killed the 0-jet sample of every p p > z @0 + jets run.""" + """A 2 -> 1 process has no clustering to find and must not raise "MLM + clustering found no valid clustering for this process", which would kill + the 0-jet sample of a p p > z @0 + jets run.""" clustering = drell_yan_two_to_one_clustering( scale_scheme=scale_scheme, clustering_history=history, xqcut=20.0 ) diff --git a/madspace/tests/test_pair_mass_floor_subtree.py b/madspace/tests/test_pair_mass_floor_subtree.py index 264938f790..ccd06f38ce 100644 --- a/madspace/tests/test_pair_mass_floor_subtree.py +++ b/madspace/tests/test_pair_mass_floor_subtree.py @@ -17,9 +17,8 @@ * W_CHANNEL: two t-channel quark lines meet in an s-channel W- that decays to the jet pair, so the cut is exactly a floor on the W- (100% efficiency). * Z_CHANNEL: a Z decays to a jet and an off-shell quark that decays to the - other jet and a W+. The jet pair is two of the Z's three leaves. Before the - floor reached such nodes only 0.04% of this channel passed the cut and the - madspace survey gave up on it ("not enough points passing cuts"). + other jet and a W+. The jet pair is two of the Z's three leaves. Without + the floor on such nodes only 0.04% of this channel passes the cut. The cut-region volume, integrated with each channel, must match the flat RAMBO mapping that knows nothing about the cut: the floor follows from the @@ -154,8 +153,8 @@ def test_z_channel_propagator_gets_the_floor(): def test_z_channel_efficiency(): """The jet pair is not a propagator of its own here, so the floor on the Z - cannot make every point pass, but it has to lift the channel out of the - 0.04% it had when only exact-pair propagators were bounded.""" + cannot make every point pass, but it has to lift the pass rate well above + the ~0.04% of the unbounded channel.""" p_free, _ = sample(PSM(topology(Z_CHANNEL), CM_ENERGY), SEED) p_cut, det = sample( PSM(topology(Z_CHANNEL), CM_ENERGY, cuts=pair_mass_cuts()), SEED @@ -299,7 +298,7 @@ def test_floor_must_hold_for_every_permutation(): mapping = PSM(x_topology(), PERM_ENERGY, cuts=x_cuts(), permutations=PERMUTATIONS) n = 20_000 p, det = sample(mapping, SEED, n, [torch.zeros(n, dtype=torch.int32)]) - # X = event (1, 2) for the identity: below the cut, but sampled + # X gets no floor, so points with m(X) below the cut are still sampled assert invariant_mass(p, 3, 4).min() < 0.5 * PERM_CUT @@ -439,7 +438,7 @@ def test_sfos_pairs_are_particle_antiparticle(): # e- e+ and mu- mu+ only, not e- mu+ nor the like-sign pairs assert values.shape[1] == 2 expected = torch.stack([invariant_mass(p, 2, 3), invariant_mass(p, 4, 5)], dim=1) - # a nearly collinear pair loses digits in m^2 = E^2 - |p|^2, hence abs + # a nearly collinear pair loses digits in m^2 = E^2 - |p|^2, hence the atol torch.testing.assert_close( values.sort(dim=1).values, expected.sort(dim=1).values, rtol=1e-9, atol=1e-6 ) @@ -495,7 +494,7 @@ def test_sfos_cannot_be_ordered(): # and an off-shell lepton, which emits a photon decaying to the last pair (the # 4-lepton output's channel 2). With every lepton pair above 50 GeV no single # pair bounds the Z beyond 50 GeV, so the root's Breit-Wigner kept sampling -# near m_Z and 0.03% of the points passed. All six pairs together put the four +# near m_Z and only ~0.03% of the points passed. All six pairs together put the four # leptons above sqrt(6) * 50 GeV, the three under the off-shell lepton above # sqrt(3) * 50 GeV. diff --git a/madspace/tests/test_pt_dr_pair_floor.py b/madspace/tests/test_pt_dr_pair_floor.py index 9d8e7a1bcc..914f9cc002 100644 --- a/madspace/tests/test_pt_dr_pair_floor.py +++ b/madspace/tests/test_pt_dr_pair_floor.py @@ -212,8 +212,8 @@ def test_no_pair_floor_for_massive_legs(): def test_no_pair_floor_beyond_pi(): - """For R > pi the minimum is no longer at d_phi = R, so the formula is not - used; the pair is then unbounded by these cuts in the sampler.""" + """For R > pi the minimum is not at d_phi = R, so the formula does not + apply; the sampler gets no pair floor from these cuts.""" r = 3.5 p_ext, _, _, _ = sample(mapping(cuts(dr_gluon=r))) m_gg = invariant_mass(p_ext, *GLUONS) diff --git a/madspace/tests/test_rank4_kernels.py b/madspace/tests/test_rank4_kernels.py index e31c6a2ed5..c6ad73d51f 100644 --- a/madspace/tests/test_rank4_kernels.py +++ b/madspace/tests/test_rank4_kernels.py @@ -1,7 +1,7 @@ """An elementwise kernel on a rank-4 tensor, batch x 2 x 3 x 4. -The GPU kernel launcher used to compute only the first slice of the last dimension -of such a tensor, leaving the other elements of the output unset. +Guards against a GPU kernel launcher that computes only the first slice of the +last dimension, leaving the other output elements unset. """ import torch diff --git a/madspace/tests/test_sqrt_s_cut.py b/madspace/tests/test_sqrt_s_cut.py index 805b48d339..8ebb0edd1f 100644 --- a/madspace/tests/test_sqrt_s_cut.py +++ b/madspace/tests/test_sqrt_s_cut.py @@ -43,7 +43,7 @@ THRESHOLD = 2 * M_TOP # below the threshold: the cut cannot bite, so nothing may change NO_OP_CUT = 300.0 -# above it: the cut removes half the points and a large enough part of the +# above it: the cut removes a large fraction of the points and enough of the # volume that a boundary which failed to compensate could not hide in the # statistics (see test_the_cut_removes_more_than_the_comparison_could_hide) BITING_CUT = 4000.0 @@ -136,7 +136,7 @@ def test_cut_bounds_the_sampled_s_hat(): assert s.min() >= BITING_CUT - 1e-6 # the region has not collapsed onto its own boundary assert s.max() > 2 * BITING_CUT - # and the cut is no longer filtering anything out + # and the cut filters nothing out assert torch.all(det != 0.0) diff --git a/madspace/tests/test_survey_job_failure.py b/madspace/tests/test_survey_job_failure.py index 998d9dd951..9f96363713 100644 --- a/madspace/tests/test_survey_job_failure.py +++ b/madspace/tests/test_survey_job_failure.py @@ -2,15 +2,13 @@ of hanging it. survey() and generate() count the jobs they have in flight and block on the -result queue until each has posted. A job that threw used to post nothing: the -thread pool caught the exception and kept it for a ThreadPool.wait() that this -loop never calls, so the main thread blocked forever at 0% CPU right after -"survey started". Seen in mg7 on a $-excluded channel whose points all fail the -veto, where ChannelEventGenerator::start_job throws "not enough points passing -cuts". - -The survey runs in a child process: with the bug it never returns, and holding -the GIL it can't be timed out from a thread. +result queue until each has posted. A job that throws must still post its +exception; if it posts nothing the main thread blocks forever. A channel whose +points all fail the cuts is such a job: ChannelEventGenerator::start_job throws +"not enough points passing cuts". + +The survey runs in a child process: a hang holds the GIL, so it cannot be timed +out from a thread. """ import os @@ -135,7 +133,6 @@ def test_survey_completes_when_every_channel_has_points(tmp_path): def test_survey_raises_when_a_job_throws(tmp_path): # pt > sqrt(s)/2 is kinematically impossible: every batch of the "dead" # channel fails the cuts, and its job throws after max_cut_repetitions. - # Before the fix this timed out. result = run_survey(tmp_path, dead_pt_min=1000.0) assert result.returncode != 0 assert "survey done" not in result.stdout diff --git a/madspace/tests/test_systematics.py b/madspace/tests/test_systematics.py index 39b55f4264..f5e06076f8 100644 --- a/madspace/tests/test_systematics.py +++ b/madspace/tests/test_systematics.py @@ -522,7 +522,8 @@ def test_mlm_pdf_ladder_follows_alpsfact_and_muf(grids, ref): last entry's scale, a variation is f(x_N-1, k S) prod_i 1 sample) keeps the plain beam ratio.""" grid, alpha_s = grids diff --git a/madspace/tests/test_t_channel.py b/madspace/tests/test_t_channel.py index 5ad050e1e9..9c3f7e278c 100644 --- a/madspace/tests/test_t_channel.py +++ b/madspace/tests/test_t_channel.py @@ -110,11 +110,9 @@ def test_t_channel_masses(masses, rng, mode): m_ext = torch.sqrt( torch.clamp(p_ext[:, :, 0] ** 2 - torch.sum(p_ext[:, :, 1:] ** 2, dim=2), min=0) ) - # The massless on-shell reconstruction m = sqrt(E^2 - |p|^2) has an FP floor - # that scales with the particle energy (~2e-6 * E); the deep color_ordered - # boost chains reach it for a few high-energy boundary events. Shallower - # modes stay well under the flat 1e-3, so only color_ordered needs the - # energy-scaled term (a genuine kinematic error would exceed it ~1000x). + # m = sqrt(E^2 - |p|^2) has a rounding floor that scales with the energy; + # the deep color_ordered boost chains reach it for a few high-energy events, + # so only color_ordered gets the energy-scaled tolerance. abs_tol = 1e-3 if mode == ms.PhaseSpaceMapping.color_ordered: abs_tol = 1e-3 + 2e-6 * torch.abs(p_ext[:, :, 0]) @@ -204,8 +202,8 @@ def test_t_channel_inverse(masses, rng, mode): det_inv = out[-1] r_inv = out[0] one_batch = torch.ones_like(det) - # Continuous randoms round-trip; the discrete int channel (color_ordered) - # is checked for exact bin/solution recovery separately. + # Continuous randoms round-trip; the discrete choices (color_ordered) are + # recovered exactly. assert_approx(r_inv, r, abs=1e-3, rel=1e-3) assert_approx(det * det_inv, one_batch, rel=1e-5) if disc is not None: @@ -241,8 +239,8 @@ def test_t_channel_phase_space_volume(particle_count, energy, rng, mode): sample_count = 100000 inputs, r, _ = _fwd_inputs(mapping, rng, sample_count) *rest, det = mapping.map_forward(inputs) - # det is the per-branch Jacobian; the solution multiplicity (2 per 2->3 peel) - # is owned externally now, so apply 2^discrete_dim to recover the full volume. + # det is the per-branch Jacobian; multiply by 2^discrete_dim (two solutions + # per 2->3 step) to get the full volume. det = det * 2.0 ** mapping.discrete_dim() ps_volume = ( (2 * math.pi) ** (4 - 3 * particle_count) @@ -263,7 +261,8 @@ def test_t_channel_phase_space_volume(particle_count, energy, rng, mode): # (beams cyclically adjacent -> all outgoing on one side). These cases also # exercise *two-sided* splits (set1 and set2 both non-empty), which build a # genuinely different t-channel topology, and a range of discrete-peel counts -# (discrete_dim grows by one for every set with > 2 outgoing partons). +# (one discrete choice per 2->3 step: each peel after the first, and a +# two-particle side when the other side is not empty). # ColorOrderedMapping is never constructed directly here -- only through # PhaseSpaceMapping, which is the only entry point it is meant to have. # --------------------------------------------------------------------------- diff --git a/madspace/tests/test_tar.py b/madspace/tests/test_tar.py index d16d8ae093..9ad878d399 100644 --- a/madspace/tests/test_tar.py +++ b/madspace/tests/test_tar.py @@ -89,7 +89,7 @@ def test_read_tar_from_other_tool(tmp_path): with tarfile.open(first) as tar: tar.extractall(src, filter="data") - # an archive written by Python's tarfile (GNU format) and by system tar + # archives written by Python's tarfile in GNU and ustar formats for fmt, name in [ (tarfile.GNU_FORMAT, "gnu.tar"), (tarfile.USTAR_FORMAT, "ustar.tar"), diff --git a/madspace/tests/test_three_body_decay.py b/madspace/tests/test_three_body_decay.py index bede8287b8..fc97780bfb 100644 --- a/madspace/tests/test_three_body_decay.py +++ b/madspace/tests/test_three_body_decay.py @@ -160,12 +160,12 @@ def test_outgoing_masses(mapping_and_args, input_points): def test_phase_space_via_two_2body(mapping_and_args, rng): - mapping_13, make_args, com = mapping_and_args # your ThreeBodyDecay + mapping_13, make_args, com = mapping_and_args N = 50000 r = rand((N, 5), generator=rng) # 5 random numbers for 1->3 decay u = r[:, 0] # first random number for s12 - # Fix parent in COM for clarity (or use your fixture’s p0) + # parent at rest with fixed masses m0, m1, m2, m3 = 120.0, 10.0, 15.0, 5.0 zeros = torch.zeros(N) p0 = torch.stack([torch.full((N,), m0), zeros, zeros, zeros], dim=1) diff --git a/madspace/tests/test_two_to_three_arcsine.py b/madspace/tests/test_two_to_three_arcsine.py index f40a651338..5f8bc70f18 100644 --- a/madspace/tests/test_two_to_three_arcsine.py +++ b/madspace/tests/test_two_to_three_arcsine.py @@ -72,7 +72,7 @@ def test_flat_weight_is_constant(): @pytest.mark.parametrize("s_power", [0.8, 1.0]) def test_weight_bounded_with_importance_sampling(s_power): - """With 1/s23^nu importance sampling the weight is no longer constant, + """With 1/s23^nu importance sampling the weight is not constant, but it must stay finite and bounded out to the edges of the range.""" *_, det = block_weights(R_S23, arcsine=True, s_power=s_power) assert torch.all(torch.isfinite(det)) and torch.all(det > 0) @@ -116,7 +116,7 @@ def test_round_trip_at_the_edges(s_power): """With the arcsine map the position in the s23 range is carried as u and 1 - u in both directions, never through s23 itself, so r_s23 is recovered to the precision of the momenta also within 1e-12 of the edges, where - going through s23 left an absolute error of ~1e-8.""" + going through s23 would lose absolute precision.""" edge = 10.0 ** -torch.arange(6.0, 13.0) r_s23 = torch.cat([edge, 1.0 - edge]) mapping, inputs, conditions, det = block_weights( diff --git a/madspace/tests/test_two_to_three_soft_limit.py b/madspace/tests/test_two_to_three_soft_limit.py index b7fbd68ee2..50b80724d2 100644 --- a/madspace/tests/test_two_to_three_soft_limit.py +++ b/madspace/tests/test_two_to_three_soft_limit.py @@ -1,20 +1,20 @@ """The 2->3 scattering block when one of its particles is soft. TwoToThreeParticleScattering samples s23 inside its kinematic range and weights -the point with the Byckling-Kajantie Jacobian 1 / (8 sqrt(-G4)). The block once -formed the 4x4 Gram determinant G4 (and cos(phi)) directly from invariants of -order s, cancelling terms of order s^4 down to a result proportional to the -square of the s23 range. Once a particle is soft that range is tiny, the -difference drowned in rounding, and the block returned Jacobians of up to -1e14 in place of O(1). One such point in a few million could throw a whole -ColorOrderedMapping integration off by a factor of up to 1e12. - -Both are now computed from where s23 lies in its range, and that range from -momenta in the p12 rest frame: its Byckling-Kajantie form through 3x3 Gram -determinants cancelled the same way one level down, and gave NaN or zero -Jacobians below a softness of about 1e-8. These tests pin that: -the Jacobian follows its smooth soft limit, and ColorOrderedMapping integrates -the massless n-body phase space to its analytic value. +the point with the Byckling-Kajantie Jacobian 1 / (8 sqrt(-G4)). Forming the 4x4 +Gram determinant G4 (and cos(phi)) directly from invariants of order s cancels +terms of order s^4 down to a result proportional to the square of the s23 range. +Once a particle is soft that range is tiny and the difference drowns in +rounding, giving Jacobians of up to 1e14 in place of O(1); one such point in a +few million could throw a ColorOrderedMapping integration off by a factor of up +to 1e12. + +The block therefore computes both from where s23 lies in its range, and that +range from momenta in the p12 rest frame: its Byckling-Kajantie form through 3x3 +Gram determinants cancels the same way one level down and gives NaN or zero +Jacobians below a softness of about 1e-8. These tests pin that the Jacobian +follows its smooth soft limit, and that ColorOrderedMapping integrates the +massless n-body phase space to its analytic value. """ import math @@ -28,8 +28,8 @@ E_BEAM = 500.0 PA = torch.tensor([E_BEAM, 0.0, 0.0, E_BEAM]) PB = torch.tensor([E_BEAM, 0.0, 0.0, -E_BEAM]) -# deep enough into the soft limit to reach where the old Gram determinant -# broke down (about 1e-8) +# deep enough into the soft limit to pass the softness (about 1e-8) where a +# Gram-determinant evaluation breaks down DELTAS = 10.0 ** -torch.arange(2.0, 15.0) @@ -97,9 +97,9 @@ def test_jacobian_with_soft_third_particle(): ids=["single chain", "1+3 split"], ) def test_color_ordered_massless_volume(color_order): - """The volume of massless 4-body phase space, (pi/2)^3 s^2 / 12. The - broken Jacobian showed up only in a few points per million, so this - needs more than a handful of batches to catch.""" + """The volume of massless 4-body phase space, (pi/2)^3 s^2 / 12. A + bad Jacobian shows up in only a few points per million, so this needs more + than a handful of batches to catch.""" cm_energy = 1000.0 n_out = len(color_order) - 2 volume = ( @@ -158,9 +158,10 @@ def extreme_points(): def test_finite_at_extreme_points(arcsine, s_power): """Forward and inverse stay finite in all these limits. Where p1 or p3 is along pa the azimuth is undefined and the weight is zero; there the s23 - range has zero width, and the inverse used to divide 0 / 0. A p3 below - ~1e-12 GeV (or a p_12 that close to rest) also hit absolute floors in the - normalisation of the frame and the boost axis and came out far off shell.""" + range has zero width, and the inverse must not divide 0 / 0. A p3 below + ~1e-12 GeV (or a p_12 that close to rest) must not hit absolute floors in the + normalisation of the frame and the boost axis, which would put the momenta + far off shell.""" points = extreme_points() n = len(points) mapping = ms.TwoToThreeParticleScattering( @@ -181,10 +182,9 @@ def test_finite_at_extreme_points(arcsine, s_power): p1, p2, det = mapping.map_forward(inputs, conditions) assert torch.all(torch.isfinite(p1)) and torch.all(torch.isfinite(p2)) assert torch.all(torch.isfinite(det)) and torch.all(det >= 0) - # On shell where the weight is not zero: the soft-p3 points used to be off - # by ~1e4 GeV^2, |p^2 - m^2| / E^2 ~ 1e-2. What is left comes from s12 -> 0, - # a pair collinear to ~1e-6 whose mass pa + pb - p3 fixes only to ~1e-4, - # and stays below ~1e-9. (At zero weight the pair mass can be below + # On shell where the weight is not zero. The residual |p^2 - m^2| / E^2 comes + # from s12 -> 0, a pair collinear to ~1e-6 whose mass pa + pb - p3 fixes only + # to ~1e-4, and stays below ~1e-9. (At zero weight the pair mass can be below # sqrt(eps) of its energy, which momenta cannot carry at all.) live = det > 0 m1 = inputs[3][live]