MARLEY (Model of Argon Reaction Low Energy Yields) v2.0.0
A Monte Carlo event generator for tens-of-MeV neutrino interactions
Loading...
Searching...
No Matches
TabulatedXSec.cc
1
4//
5// This file is part of MARLEY (Model of Argon Reaction Low Energy Yields)
6//
7// MARLEY is free software: you can redistribute it and/or modify it under the
8// terms of version 3 of the GNU General Public License as published by the
9// Free Software Foundation.
10//
11// For the full text of the license please see COPYING or
12// visit http://opensource.org/licenses/GPL-3.0
13//
14// Please respect the MCnet academic usage guidelines. See GUIDELINES
15// or visit https://www.montecarlonet.org/GUIDELINES for details.
16
17// Standard library includes
18#include <fstream>
19
20// MARLEY includes
21#include "marley/Error.hh"
22#include "marley/FileManager.hh"
23#include "marley/Logger.hh"
24#include "marley/MassTable.hh"
25#include "marley/Reaction.hh"
26#include "marley/TabulatedXSec.hh"
27#include "marley/marley_utils.hh"
28
29marley::TabulatedXSec::TabulatedXSec( int target_pdg,
30 marley::Reaction::ProcessType p_type, CoulombCorrector::CoulombMode mode,
31 double delta_ias ) : ta_( target_pdg ), proc_type_( p_type ),
32 coulomb_mode_( mode ), delta_ias_( delta_ias )
33{
34 // Set the residue PDG code (pdg_d_) based on the input information
35 int dummy_charge;
37 pdg_d_, dummy_charge );
38}
39
40void marley::TabulatedXSec::add_table( const std::string& file_name )
41{
42 const auto& fm = marley::FileManager::Instance();
43 std::string full_file_name = fm.find_file( file_name );
44 if ( full_file_name.empty() ) {
45 throw marley::Error( "Could not open the nuclear response data file \""
46 + file_name + '\"' );
47 }
48
49 std::ifstream in_file( full_file_name );
50
51 // TODO: add error handling for file parsing
52
53 // Multipole order
54 unsigned J;
55 in_file >> J;
56
57 // Grid sizes
58 unsigned num_w, num_q;
59
60 // Temporary storage for grid points
61 double w, q;
62
63 // 1D grids
64 auto wvec = std::make_shared< std::vector<double> >();
65 auto qvec = std::make_shared< std::vector<double> >();
66
67 in_file >> num_w;
68 for ( unsigned iw = 0u; iw < num_w; ++iw ) {
69 in_file >> w;
70 wvec->push_back( w );
71 }
72
73 in_file >> num_q;
74 for ( unsigned iq = 0u; iq < num_q; ++iq ) {
75 in_file >> q;
76 qvec->push_back( q );
77 }
78
79 // Natural parity is given by (-1)^J, while unnatural parity is the opposite
80 marley::Parity natural;
81 bool J_is_odd = (J % 2 == 1);
82 if ( J_is_odd ) natural = -1;
83 else natural = 1;
84
85 marley::Parity unnatural = -natural;
86
87 MultipoleLabel nat_ml( J, natural );
88 MultipoleLabel unnat_ml( J, unnatural );
89
90 auto nat_resp = std::make_shared<
91 std::vector<marley::NuclearResponses> >();
92
93 auto unnat_resp = std::make_shared<
94 std::vector<marley::NuclearResponses> >();
95
96 // Temporary storage for responses
97 double rcc, rll, rcl, rtVV, rtAA, rtprime;
98
99 // Flag used to swap back and forth between natural and unnatural parity
100 bool nat = true;
101 while ( in_file >> rcc >> rll >> rcl >> rtVV >> rtAA >> rtprime ) {
102
103 if ( nat ) nat_resp->emplace_back( rcc, rll, rcl, rtVV, rtAA, rtprime );
104 else unnat_resp->emplace_back( rcc, rll, rcl, rtVV, rtAA, rtprime );
105
106 // Flip the parity flag
107 nat = !nat;
108 }
109
110 // Store the finished tables as new objects in the map
111 responses_[ nat_ml ] = ResponseTable( wvec, qvec, nat_resp );
112 responses_[ unnat_ml ] = ResponseTable( wvec, qvec, unnat_resp );
113
114}
115
116double marley::TabulatedXSec::diff_xsec( int pdg_a, double KEa, double omega,
117 double cos_theta, const marley::TabulatedXSec::MultipoleLabel& ml ) const
118{
119 int helicity = marley_utils::get_particle_helicity( pdg_a );
120
121 // Look up the masses of the projectile and ejectile
122 int pdg_c = marley::Reaction::get_ejectile_pdg( pdg_a, proc_type_ );
123 const auto& mt = marley::MassTable::Instance();
124 double ma = mt.get_particle_mass( pdg_a );
125 double mc = mt.get_particle_mass( pdg_c );
126
127 // Determine the total energies and momenta of the initial and final leptons
128 double Ea = KEa + ma;
129 double pa = marley_utils::real_sqrt( Ea*Ea - ma*ma );
130 double Ec = Ea - omega;
131 if ( Ec < mc ) return 0.;
132
133 double pc = marley_utils::real_sqrt( Ec*Ec - mc*mc );
134
135 // Get the magnitude of the 3-momentum transfer (q) from this information
136 // and the scattering cosine. If the scattering cosine is unphysical, then
137 // just return zero.
138 if ( std::abs(cos_theta) > 1. ) return 0.;
139 double q = marley_utils::real_sqrt( pa*pa + pc*pc - 2.*pa*pc*cos_theta );
140
141 // Get the table of pre-computed nuclear responses for the requested
142 // multipole
143 const auto& rt = this->responses_.at( ml );
144
145 // Shift the energy transfer at which the responses are evaluated. This
146 // effective value of the energy transfer tries to correct for using the
147 // same nuclear potential for the initial and final states despite
148 // a change of nuclear charge for charged-current interactions.
149 double omega_eff = omega + this->delta_ias();
150
151 // Interpolate a set of nuclear responses for the given omega_eff and q values
152 if ( omega_eff < rt.w_min() || omega_eff > rt.w_max()
153 || q < rt.q_min() || q > rt.q_max() ) return 0.;
154 auto nr = rt.interpolate( omega_eff, q );
155
156 // Compute the lepton factors
157 double beta = pc / Ec;
158 double sin_theta2 = 1. - cos_theta*cos_theta;
159 double q2 = q*q;
160 double vcc = 1. + beta * cos_theta;
161 double vll = vcc - 2.*Ea*Ec*sin_theta2*beta*beta/q2;
162 double vcl = -2. * ( omega*vcc/q + mc*mc/Ec/q );
163 double vT = 1. - beta*cos_theta + Ea*Ec*beta*beta*sin_theta2/q2;
164 double vTprime = 2. * helicity * ( (Ea + Ec)*(1 - beta*cos_theta)/q
165 - mc*mc/q/Ec );
166 LeptonFactors lf( vcc, vll, vcl, vT, vTprime );
167
168 // Compute the double-differential cross section with respect to
169 // the energy transfer and the scattering cosine
170 double xsec = lf * nr;
171 xsec *= 2. * marley_utils::GF2 * marley_utils::Vud2 * Ec * pc;
172
173 // For CC cross sections, apply the appropriate correction factor for
174 // Coulomb corrections based on the active "Coulomb mode"
175 bool is_charged_current = this->is_cc();
176
177 if ( is_charged_current ) {
178
179 // Get the mass of the atomic target
180 double mb = mt.get_atomic_mass( ta_.pdg() );
181
182 // Lab-frame total energy of the residue (via energy conservation)
183 double Ed = omega + mb;
184
185 // Mass of the residue (via 3-momentum conservation)
186 double md = marley_utils::real_sqrt( Ed*Ed - q*q );
187
188 // Dot product of the 4-momenta of the ejectile and residue
189 double pc_dot_pd = Ec*Ed + pc*pc - pa*pc*cos_theta;
190
191 // Manifestly Lorentz-invariant relative speed of particles c and d
192 double beta_rel_cd = marley_utils::real_sqrt(
193 std::pow(pc_dot_pd, 2) - mc*mc*md*md ) / pc_dot_pd;
194
195 // Compute the Coulomb correction factor using the relative speed
196 CoulombCorrector coul_corr( pdg_c, pdg_d_, coulomb_mode_ );
197 double FC = coul_corr.coulomb_correction_factor( beta_rel_cd );
198
199 // Apply it to the differential cross section
200 xsec *= FC;
201 }
202
203 return xsec;
204}
205
206double marley::TabulatedXSec::compute_integral( int pdg_a, double KEa,
207 const marley::TabulatedXSec::MultipoleLabel& ml, double& diff_max,
208 std::vector< IntegralTerm >* integral_terms ) const
209{
210 // Set the maximum differential cross section to zero to start
211 diff_max = 0.;
212
213 // If the input vector is not null, then clear it in preparation for
214 // accumulating the terms of the integral
215 if ( integral_terms ) integral_terms->clear();
216
217 // Get the table of nuclear responses for the requested multipole
218 const auto& rt = this->responses_.at( ml );
219
220 // Get the vectors of grid points
221 const auto& wvec = rt.w_grid();
222 const auto& qvec = rt.q_grid();
223
224 // TODO: reduce code duplication with this and diff_xsec
225 // Look up the masses of the projectile and ejectile
227 const auto& mt = marley::MassTable::Instance();
228 double ma = mt.get_particle_mass( pdg_a );
229 double mc = mt.get_particle_mass( pdg_c );
230
231 // Projectile total energy
232 double Ea = KEa + ma;
233
234 // Get the grid step sizes
235 // NOTE: this assumes that the grid is regularly spaced (I take advantage
236 // of this to simplify the trapezoidal rule for integration)
237 // TODO: revisit this assumption
238 size_t num_w = wvec.size();
239 size_t num_q = qvec.size();
240
241 size_t num_w_minus_one = num_w - 1;
242 size_t num_q_minus_one = num_q - 1;
243
244 double dw = ( wvec.back() - wvec.front() ) / num_w_minus_one;
245 double dq = ( qvec.back() - qvec.front() ) / num_q_minus_one;
246
247 // Loop over every grid point. Do an angular integral at every omega
248 // point, and use the results to integrate over omega.
249 double integ = 0.;
250 for ( size_t iw = 0u; iw < num_w; ++iw ) {
251
252 // Get the energy transfer at the current grid point. Note that the
253 // tables of nuclear responses are reported on a grid that uses the
254 // effective value (shifted by delta_ias_). We correct for this here.
255 double w_eff = wvec.at( iw );
256 double w = w_eff - this->delta_ias();
257
258 // Use it to compute the ejectile total energy, etc.
259 double Ec = Ea - w;
260 // We can skip unphysical terms for which the total energy is smaller than
261 // the final lepton mass
262 if ( Ec < mc ) continue;
263 double pc = marley_utils::real_sqrt( Ec*Ec - mc*mc );
264 double pa = marley_utils::real_sqrt( Ea*Ea - ma*ma );
265
266 double w_integ = 0.;
267 for ( size_t iq = 0u; iq < num_q; ++iq ) {
268
269 // Get the magnitude of the 3-momentum transfer at the current grid point
270 double q = qvec.at( iq );
271
272 // Convert it into a value for the scattering cosine
273 double cos_theta = ( pa*pa + pc*pc - q*q ) / ( 2.*pa*pc );
274
275 // Compute the differential cross section at this 2D grid point
276 double diff = this->diff_xsec( pdg_a, KEa, w, cos_theta, ml );
277
278 // If it is larger than any value encountered so far, record it as
279 // the new maximum
280 if ( diff > diff_max ) diff_max = diff;
281
282 // Apply a Jacobian to transform from dwdcostheta to dwdq (since we're
283 // integrating on a grid of q values)
284 diff *= q / pa / pc;
285
286 // Add this term to the integral over omega according to the trapezoid
287 // rule
288 if ( iq == 0u || iq == num_q_minus_one ) diff /= 2.;
289 w_integ += diff;
290
291 // If the user supplied a vector to record the terms in the integral,
292 // then store the contribution from the current iteration before
293 // continuing to the next grid point
294 if ( integral_terms ) {
295 double value = diff * dq * dw;
296 if ( iw == 0u || iw == num_w_minus_one ) value /= 2.;
297 integral_terms->emplace_back( w, cos_theta, value );
298 }
299
300 } // q grid points
301
302 // We're done summing over q values. Scale the result to get the
303 // integral over q for this w grid point.
304 w_integ *= dq;
305
306 // Now add this term to the integral over w according to the trapezoid rule
307 if ( iw == 0u || iw == num_w_minus_one ) w_integ /= 2.;
308
309 integ += w_integ;
310
311 } // w grid points
312
313 // We're done. Scale the integral over w by the needed prefactor
314 integ *= dw;
315
316 return integ;
317}
318
319double marley::TabulatedXSec::integral( int pdg_a, double KEa,
320 const marley::TabulatedXSec::MultipoleLabel& ml, double& diff_max ) const
321{
322 // First attempt to look up a pair of ChebyshevInterpolatingFunction
323 // objects for the given projectile PDG code and multipole
324 OptimizationMapKey key( pdg_a, ml );
325 auto end = optimization_map_.end();
326 auto iter = optimization_map_.find( key );
327 // If we found one, attempt to compute the cross sections using them
328 if ( iter != end ) {
329 const auto& omv = iter->second;
330 const auto& tot = omv.tot_xsec_;
331 // If the requested kinetic energy is below threshold, then just
332 // return zero
333 if ( KEa < tot.x_min() ) return 0.;
334 // If the kinetic energy is within the range covered by the
335 // interpolating functions, then go ahead and use them
336 else if ( KEa <= tot.x_max() ) {
337 double tot_xsec = omv.tot_xsec_.evaluate( KEa );
338 diff_max = omv.max_diff_xsec_.evaluate( KEa );
339
340 // Ensure that interpolation problems will not give us a negative value
341 // for the total or maximum differential cross sections
342 tot_xsec = std::max( 0., tot_xsec );
343 diff_max = std::max( 0., diff_max );
344
345 return tot_xsec;
346 }
347 }
348
349 // If we couldn't find a suitable interpolating function, then fall back to
350 // brute-force integration
351 return this->compute_integral( pdg_a, KEa, ml, diff_max );
352}
353
354double marley::TabulatedXSec::integral( int pdg_a, double KEa ) const
355{
356 double integ = 0.;
357 for ( const auto& pair : responses_ ) {
358 double dummy;
359 const auto& ml = pair.first;
360 double integ_ml = this->integral( pdg_a, KEa, ml, dummy );
361 integ += integ_ml;
362 }
363 return integ;
364}
365
366void marley::TabulatedXSec::optimize( int pdg_a, double max_KEa ) {
367 MARLEY_LOG( INFO, "physics.reaction.xsec" ) << "Optimizing CRPA cross"
368 " section up to " << max_KEa << " MeV";
369 // Loop over each of the multipoles
370 for ( const auto& pair : responses_ ) {
371 const auto& ml = pair.first;
372
373 double min_KEa = 0.;
374 std::function<double(double)> tot_xsec_func = [](double)
375 -> double { return 0.; };
376 std::function<double(double)> max_diff_xsec_func = tot_xsec_func;
377
378 // Verify that the total cross section for this multipole is non-vanishing
379 // for the requested maximum projectile kinetic energy KEa. If it vanishes,
380 // then just skip the current multipole.
381 double dummy;
382 double xsec_at_max = this->integral( pdg_a, max_KEa, ml, dummy );
383
384 if ( xsec_at_max > 0. ) {
385 // Do a binary search to find the threshold for this multipole. Continue
386 // until we've found it within the given tolerance.
387 constexpr double thresh_tol = 1e-6; // MeV
388 // Set up the bounds of a bracketing interval that will contain the
389 // kinetic energy threshold
390 double low_KEa = 0.;
391 double high_KEa = max_KEa;
392 do {
393 // Check the total cross section at the midpoint of the current
394 // bracketing interval
395 double cur_KEa = (low_KEa + high_KEa) / 2.;
396 double xsec = this->integral( pdg_a, cur_KEa, ml, dummy );
397 // If it vanishes, move the lower bound up
398 if ( xsec <= 0. ) low_KEa = cur_KEa;
399 // If it doesn't, move the upper bound down
400 else high_KEa = cur_KEa;
401 // Continue until the bracketing interval is no larger than the
402 // tolerance defined above
403 } while ( std::abs(high_KEa - low_KEa) > thresh_tol );
404
405 // Adopt the lower bound of the bracketing interval as the threshold
406 min_KEa = low_KEa;
407
408 tot_xsec_func = [&, this](double KEa)
409 -> double { return this->integral( pdg_a, KEa, ml, dummy ); };
410
411 max_diff_xsec_func = [&, this](double KEa) -> double {
412 double max_diff;
413 this->integral( pdg_a, KEa, ml, max_diff );
414 return max_diff;
415 };
416 }
417
418 MARLEY_LOG( DEBUG, "physics.reaction.xsec" ) << "Optimizing total cross section for "
419 << ml.J_ << ml.Pi_ << " over KE in ["
420 << min_KEa << ", " << max_KEa << "] MeV";
421
422 // Now we're ready to build the Chebyshev interpolating functions for
423 // this multipole. We need one for the total cross section and the
424 // other one for the maximum of the differential cross section.
425 ChebyshevInterpolatingFunction tot_xs_cif( tot_xsec_func, min_KEa,
426 max_KEa, 64 );
427 // TODO: do you want adaptive grid sizing here?
428
429 MARLEY_LOG( DEBUG, "physics.reaction.xsec" ) << "Optimizing max diff for "
430 << ml.J_ << ml.Pi_;
431
432 ChebyshevInterpolatingFunction max_diff_xs_cif( max_diff_xsec_func,
433 min_KEa, max_KEa, 64 );
434
435 // Build the key and value we need for the map entry
436 OptimizationMapKey key( pdg_a, ml );
437 OptimizationMapValue value( tot_xs_cif, max_diff_xs_cif );
438
439 optimization_map_.emplace( std::make_pair(key, value) );
440 }
441}
442
443// For CC interactions, apply an energy shift to account for the difference in
444// energy between the ground state of the initial nucleus and the isobaric
445// analog state in the final nucleus. This leads to an effective energy
446// transfer value omega_eff which is used when evaluating the final-state
447// lepton energy.
449 bool is_charged_current = this->is_cc();
450
451 double result = 0.;
452 if ( is_charged_current ) result = delta_ias_;
453 return result;
454}
Approximates a 1D function using Chebyshev points.
Computes Coulomb correction factors for neutrino-nucleus differential cross sections.
static const FileManager & Instance()
Get a const reference to the singleton instance of the FileManager.
static const MassTable & Instance()
Get a const reference to the singleton instance of the MassTable.
Definition MassTable.cc:69
static int get_ejectile_pdg(int pdg_a, ProcessType proc_type)
Definition Reaction.cc:329
ProcessType
Enumerated type describing the kind of scattering process represented by a Reaction.
Definition Reaction.hh:58
static void get_residue_pdg_and_charge(ProcessType proc_type, int pdg_b, int &pdg_d, int &q_d)
Definition Reaction.cc:573
marley::Reaction::ProcessType proc_type_
Kind of process for which the cross section will be computed.
double delta_ias() const
Get the shift used to compute the effective energy transfer.
std::map< MultipoleLabel, ResponseTable > responses_
Tables of nuclear responses organized by multipole.
double compute_integral(int pdg_a, double KEa, const MultipoleLabel &ml, double &diff_max, std::vector< IntegralTerm > *integral_terms=nullptr) const
Helper function for integral that does the actual integration.
bool is_cc() const
Returns true if this cross section represents a CC process or false otherwise.
Simple struct representing a given multipole order, e.g., 2+.