File indexing completed on 2026-10-16 08:30:14
0001 #include "TpcPrimaryIonizationModel.h"
0002
0003 #include <algorithm>
0004 #include <array>
0005 #include <cmath>
0006 #include <iterator>
0007 #include <stdexcept>
0008 #include <vector>
0009
0010 namespace
0011 {
0012 constexpr double argon_primary_clusters_per_cm = 23.0;
0013 constexpr double cf4_primary_clusters_per_cm = 51.0;
0014 constexpr double isobutane_primary_clusters_per_cm = 84.0;
0015
0016
0017
0018
0019 constexpr std::array<double, 19> argon_cluster_probabilities = {
0020 0.656, 0.150, 0.064, 0.035, 0.0225, 0.0155, 0.0105,
0021 0.0081, 0.0061, 0.0049, 0.0039, 0.0030, 0.0025, 0.0020,
0022 0.0016, 0.0012, 0.00095, 0.00075, 0.00063};
0023
0024 constexpr std::array<double, 19> methane_cluster_probabilities = {
0025 0.786, 0.120, 0.034, 0.016, 0.0095, 0.0060, 0.0044,
0026 0.0034, 0.0027, 0.0021, 0.0017, 0.0013, 0.0010, 0.0008,
0027 0.0006, 0.00050, 0.00042, 0.00037, 0.00033};
0028
0029 template <std::size_t Size>
0030 std::vector<double> build_cluster_probabilities(
0031 const std::array<double, Size> &measured)
0032 {
0033 std::vector<double> probabilities(
0034 TpcPrimaryIonizationModel::maximum_cluster_size + 1, 0.0);
0035
0036 double measured_sum = 0.0;
0037 for (std::size_t index = 0; index < measured.size(); ++index)
0038 {
0039 probabilities[index + 1] = measured[index];
0040 measured_sum += measured[index];
0041 }
0042
0043 if (!(measured_sum > 0.0 && measured_sum <= 1.0))
0044 {
0045 throw std::logic_error("invalid measured TPC cluster-size probabilities");
0046 }
0047
0048
0049
0050 constexpr unsigned int tail_begin = Size + 1;
0051 double inverse_square_sum = 0.0;
0052 for (unsigned int size = tail_begin;
0053 size <= TpcPrimaryIonizationModel::maximum_cluster_size;
0054 ++size)
0055 {
0056 inverse_square_sum += 1.0 / static_cast<double>(size * size);
0057 }
0058
0059 const double tail_normalization =
0060 (1.0 - measured_sum) / inverse_square_sum;
0061 for (unsigned int size = tail_begin;
0062 size <= TpcPrimaryIonizationModel::maximum_cluster_size;
0063 ++size)
0064 {
0065 probabilities[size] =
0066 tail_normalization / static_cast<double>(size * size);
0067 }
0068
0069 return probabilities;
0070 }
0071 }
0072
0073 TpcPrimaryIonizationModel::TpcPrimaryIonizationModel(
0074 const GasFractions &fractions)
0075 {
0076 const std::array<double, 3> values = {
0077 fractions.argon, fractions.cf4, fractions.isobutane};
0078 for (const double fraction : values)
0079 {
0080 if (!std::isfinite(fraction) || fraction < 0.0)
0081 {
0082 throw std::invalid_argument(
0083 "TPC primary-ionization gas fractions must be finite and nonnegative");
0084 }
0085 }
0086
0087 const double fraction_sum =
0088 fractions.argon + fractions.cf4 + fractions.isobutane;
0089 if (!std::isfinite(fraction_sum) || std::abs(fraction_sum - 1.0) > 1.e-6)
0090 {
0091 throw std::invalid_argument(
0092 "TPC primary-ionization gas fractions must sum to one");
0093 }
0094
0095 const double argon_weight =
0096 fractions.argon * argon_primary_clusters_per_cm;
0097 const double cf4_weight = fractions.cf4 * cf4_primary_clusters_per_cm;
0098 const double isobutane_weight =
0099 fractions.isobutane * isobutane_primary_clusters_per_cm;
0100 m_primaryClustersPerCm = argon_weight + cf4_weight + isobutane_weight;
0101
0102 if (!(m_primaryClustersPerCm > 0.0))
0103 {
0104 throw std::invalid_argument(
0105 "TPC primary-ionization gas mixture has zero cluster density");
0106 }
0107
0108 const auto argon_probabilities =
0109 build_cluster_probabilities(argon_cluster_probabilities);
0110 const auto methane_probabilities =
0111 build_cluster_probabilities(methane_cluster_probabilities);
0112
0113
0114
0115
0116
0117 const double argon_shape_weight =
0118 (argon_weight + cf4_weight) / m_primaryClustersPerCm;
0119 const double methane_shape_weight =
0120 isobutane_weight / m_primaryClustersPerCm;
0121
0122 m_clusterSizeCdf.resize(maximum_cluster_size + 1, 0.0);
0123 double cumulative_probability = 0.0;
0124 for (unsigned int size = 1; size <= maximum_cluster_size; ++size)
0125 {
0126 cumulative_probability +=
0127 argon_shape_weight * argon_probabilities[size] +
0128 methane_shape_weight * methane_probabilities[size];
0129 m_clusterSizeCdf[size] = cumulative_probability;
0130 }
0131 m_clusterSizeCdf[maximum_cluster_size] = 1.0;
0132 }
0133
0134 double TpcPrimaryIonizationModel::mean_primary_clusters(
0135 const double path_length_cm) const
0136 {
0137 if (!std::isfinite(path_length_cm) || path_length_cm < 0.0)
0138 {
0139 throw std::invalid_argument(
0140 "TPC primary-ionization path length must be finite and nonnegative");
0141 }
0142 return m_primaryClustersPerCm * path_length_cm;
0143 }
0144
0145 unsigned int TpcPrimaryIonizationModel::sample_cluster_size(
0146 const double uniform_01) const
0147 {
0148 if (!std::isfinite(uniform_01) || uniform_01 < 0.0 || uniform_01 >= 1.0)
0149 {
0150 throw std::invalid_argument(
0151 "TPC cluster-size random variate must lie in [0, 1)");
0152 }
0153
0154 const auto found = std::lower_bound(
0155 std::next(m_clusterSizeCdf.begin()),
0156 m_clusterSizeCdf.end(),
0157 uniform_01);
0158 return static_cast<unsigned int>(
0159 std::distance(m_clusterSizeCdf.begin(), found));
0160 }
0161
0162 double TpcPrimaryIonizationModel::mean_cluster_size() const
0163 {
0164 double previous_cdf = 0.0;
0165 double mean = 0.0;
0166 for (unsigned int size = 1; size <= maximum_cluster_size; ++size)
0167 {
0168 const double probability = m_clusterSizeCdf[size] - previous_cdf;
0169 mean += static_cast<double>(size) * probability;
0170 previous_cdf = m_clusterSizeCdf[size];
0171 }
0172 return mean;
0173 }