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
cmd_decay.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#include <iostream>
18#include <sstream>
19#include <string>
20#include <vector>
21
22#include "HepMC3/Attribute.h"
23#include "HepMC3/GenEvent.h"
24#include "HepMC3/GenParticle.h"
25#include "HepMC3/GenVertex.h"
26
27#include "marley/CommandHandler.hh"
28#include "marley/Error.hh"
29#include "marley/Generator.hh"
30#include "marley/JSON.hh"
31#include "marley/JSONConfig.hh"
32#include "marley/Logger.hh"
33#include "marley/MassTable.hh"
34#include "marley/NucleusDecayer.hh"
35#include "marley/OutputFile.hh"
36#include "marley/Parity.hh"
37#include "marley/Reaction.hh"
38#include "marley/hepmc3_utils.hh"
39#include "marley/marley_utils.hh"
40
41using ProcType = marley::Reaction::ProcessType;
42
43// ---------------------------------------------------------------------------
44// Helper: derive Z and A from a nuclear PDG code.
45// Delegates to the canonical marley_utils helpers, which already handle the
46// neutron (2112), proton (2212), and general nuclear ion cases.
47// Returns false if the PDG code is not recognized as a nucleus or nucleon.
48// ---------------------------------------------------------------------------
49static bool pdg_to_ZA( int pdg, int& Z, int& A ) {
50 if ( pdg == marley_utils::NEUTRON || pdg == marley_utils::PROTON
51 || marley_utils::is_ion(pdg) )
52 {
53 Z = marley_utils::get_particle_Z( pdg );
54 A = marley_utils::get_particle_A( pdg );
55 return true;
56 }
57 return false;
58}
59
60bool marley::CommandHandler::cmd_decay( std::deque< std::string >& args ) {
61
62 std::string config_file_name;
63 if ( !args.empty() ) config_file_name = args.front();
64
65 if ( config_file_name.empty() || config_file_name == "-h"
66 || config_file_name == "--help" )
67 {
68 args.clear();
69 args.push_front( "decay" );
71 }
72
73 // -------------------------------------------------------------------------
74 // Load the JSON config from file, then patch in null entries for "reactions"
75 // and "source" if those keys are absent. This allows create_generator() to
76 // take its early-return path and skip cross-section normalisation, which is
77 // irrelevant for standalone de-excitation events.
78 // -------------------------------------------------------------------------
79 marley::JSON json = marley::JSON::load_file( config_file_name );
80
81 if ( !json.has_key("reactions") ) json["reactions"] = marley::JSON( nullptr );
82 if ( !json.has_key("source") ) json["source"] = marley::JSON( nullptr );
83
84 marley::JSONConfig jc( json );
85 marley::Generator gen = jc.create_generator();
86 gen.set_up_run_info();
87
88 bool ok;
89
90 // -------------------------------------------------------------------------
91 // Parse the required top-level "decay" block
92 // -------------------------------------------------------------------------
93 const std::string decay_config_label( "decay" );
94 marley::JSON decay_config;
95 ok = get_from_json< marley::JSON >( decay_config_label, json, decay_config );
96 if ( !ok ) throw marley::Error( "Missing key '" + decay_config_label
97 + "' in job configuration file" );
98
99 long num_events = assign_from_json< long >( "events", decay_config, ok, 1000 );
100
101 // -------------------------------------------------------------------------
102 // Parse the required "nucleus" sub-object
103 // -------------------------------------------------------------------------
104 if ( !decay_config.has_key("nucleus") ) {
105 throw marley::Error( "Missing required \"nucleus\" key in the"
106 " \"decay\" configuration block" );
107 }
108 const marley::JSON& nuc_config = decay_config.at( "nucleus" );
109
110 // Read the nucleus identity. The user may supply either (or both) of:
111 // Option A: Z and A integer keys
112 // Option B: pdg integer key (nuclear PDG code)
113 // If both are supplied they must be consistent.
114
115 bool has_pdg = nuc_config.has_key("pdg");
116 bool has_Z = nuc_config.has_key("Z");
117 bool has_A = nuc_config.has_key("A");
118
119 if ( !has_pdg && !( has_Z && has_A ) ) {
120 throw marley::Error( "The \"nucleus\" block must specify the nuclide"
121 " using either the \"pdg\" key or both the \"Z\" and \"A\" keys" );
122 }
123
124 int nucleus_pdg = 0;
125 int Z = 0, A = 0;
126
127 if ( has_pdg ) {
128 nucleus_pdg = assign_from_json< int >( "pdg", nuc_config, ok );
129 int Z_from_pdg = 0, A_from_pdg = 0;
130 if ( !pdg_to_ZA( nucleus_pdg, Z_from_pdg, A_from_pdg ) ) {
131 throw marley::Error( "The value " + std::to_string(nucleus_pdg)
132 + " given for \"nucleus.pdg\" is not a recognized nuclear"
133 " or nucleon PDG code" );
134 }
135 Z = Z_from_pdg;
136 A = A_from_pdg;
137 }
138
139 if ( has_Z && has_A ) {
140 int Z_cfg = assign_from_json< int >( "Z", nuc_config, ok );
141 int A_cfg = assign_from_json< int >( "A", nuc_config, ok );
142 if ( has_pdg ) {
143 // Both representations present — verify consistency
144 if ( Z_cfg != Z || A_cfg != A ) {
145 throw marley::Error( "Inconsistent nucleus specification: pdg="
146 + std::to_string(nucleus_pdg) + " implies Z=" + std::to_string(Z)
147 + ", A=" + std::to_string(A) + ", but Z=" + std::to_string(Z_cfg)
148 + ", A=" + std::to_string(A_cfg) + " were also given" );
149 }
150 }
151 else {
152 Z = Z_cfg;
153 A = A_cfg;
154 nucleus_pdg = marley_utils::get_nucleus_pid( Z, A );
155 }
156 }
157
158 if ( Z < 0 ) throw marley::Error( "Negative Z encountered" );
159 if ( A < 1 ) throw marley::Error( "A < 1 encountered" );
160
161 // Net ionic charge of the nucleus (protons minus electrons).
162 // Default 0 = neutral atom.
163 int net_charge = assign_from_json< int >( "net_charge", nuc_config, ok, 0 );
164
165 // -------------------------------------------------------------------------
166 // Parse excitation energy: fixed value or uniform sampling interval
167 // -------------------------------------------------------------------------
168 double Ex = 0.;
169 double Ex_min = 0.;
170 double Ex_max = 0.;
171 bool sample_Ex = nuc_config.has_key( "Ex_max" );
172 if ( !sample_Ex ) {
173 Ex = assign_from_json< double >( "Ex", nuc_config, ok, -1.0 );
174 if ( !ok ) throw marley::Error( "Missing \"Ex\" key in \"nucleus\" block"
175 " (or use \"Ex_min\" + \"Ex_max\" for uniform sampling)" );
176 if ( Ex < 0. ) throw marley::Error( "Negative excitation energy"
177 " encountered (Ex = " + std::to_string(Ex) + " MeV)" );
178 }
179 else {
180 Ex_min = assign_from_json< double >( "Ex_min", nuc_config, ok, -1.0 );
181 Ex_max = assign_from_json< double >( "Ex_max", nuc_config, ok, -1.0 );
182 if ( Ex_min < 0. ) throw marley::Error( "Negative lower excitation"
183 " energy bound (Ex_min = " + std::to_string(Ex_min) + " MeV)" );
184 if ( Ex_max < 0. ) throw marley::Error( "Negative upper excitation"
185 " energy bound (Ex_max = " + std::to_string(Ex_max) + " MeV)" );
186 if ( Ex_max < Ex_min ) throw marley::Error( "Upper Ex bound"
187 " (" + std::to_string(Ex_max) + " MeV) is less than lower Ex bound"
188 " (" + std::to_string(Ex_min) + " MeV)" );
189 }
190
191 // -------------------------------------------------------------------------
192 // Parse nuclear spin: required array of 2J values
193 // -------------------------------------------------------------------------
194 std::vector< int > twoJ_vec;
195 if ( !nuc_config.has_key("twoJ") ) {
196 throw marley::Error( "Missing \"twoJ\" key in \"nucleus\" block" );
197 }
198 else {
199 const marley::JSON& twoJ_obj = nuc_config.at( "twoJ" );
200 if ( !twoJ_obj.is_array() ) {
201 throw marley::Error( "The \"twoJ\" key in \"nucleus\" must have"
202 " a value that is a JSON array" );
203 }
204 convert_json< std::vector<int> >( twoJ_obj, twoJ_vec );
205 }
206 if ( twoJ_vec.empty() ) {
207 throw marley::Error( "The \"twoJ\" array in \"nucleus\" must not"
208 " be empty" );
209 }
210 for ( const auto& tJ : twoJ_vec ) {
211 if ( tJ < 0 ) throw marley::Error( "Negative 2J value encountered"
212 " in \"twoJ\" array" );
213 bool even_A = ( A % 2 == 0 );
214 bool even_twoJ = ( tJ % 2 == 0 );
215 if ( even_A != even_twoJ ) throw marley::Error( "Unphysical twoJ = "
216 + std::to_string( tJ ) + " encountered for A = "
217 + std::to_string( A ) );
218 }
219
220 std::vector< double > twoJ_weights( twoJ_vec.size(), 1. );
221 std::discrete_distribution< size_t > twoJ_dist(
222 twoJ_weights.cbegin(), twoJ_weights.cend() );
223
224 // -------------------------------------------------------------------------
225 // Parse parity: optional, default "+"
226 // -------------------------------------------------------------------------
227 auto parity_str = assign_from_json< std::string >( "parity", nuc_config,
228 ok, "+" );
229 if ( parity_str != "+" && parity_str != "-" && parity_str != "random" ) {
230 throw marley::Error( "Invalid \"parity\" setting \""
231 + parity_str + "\" in \"nucleus\" block."
232 " Expected \"+\", \"-\", or \"random\"" );
233 }
234
235 const std::vector< std::string > parity_strings = { "+", "-" };
236 const std::vector< double > parity_weights = { 1., 1. };
237 std::discrete_distribution< size_t > parity_dist(
238 parity_weights.cbegin(), parity_weights.cend() );
239
240 // -------------------------------------------------------------------------
241 // Parse output configuration
242 // -------------------------------------------------------------------------
243 std::vector< std::shared_ptr<marley::OutputFile> > output_files;
244
245 if ( decay_config.has_key("output") ) {
246 marley::JSON output_set = decay_config.at( "output" );
247 if ( !output_set.is_array() ) throw marley::Error( "The"
248 " \"output\" key must have a value that is a JSON array." );
249 for ( const auto& el : output_set.array_range() ) {
250 output_files.push_back( marley::OutputFile::make_OutputFile(el) );
251 }
252 }
253 else {
254 std::string out_cfg_str = "{ format: \"ascii\","
255 " file: \"decay_events.hepmc3\", mode: \"overwrite\" }";
256 auto out_cfg = marley::JSON::load( out_cfg_str );
257 output_files.push_back( marley::OutputFile::make_OutputFile(out_cfg) );
258 }
259
260 for ( const auto& file : output_files ) {
261 if ( file->mode_is_resume() ) {
262 throw marley::Error( "The \"resume\" output mode is not supported"
263 " by the \"marley decay\" command" );
264 }
265 }
266
267 // -------------------------------------------------------------------------
268 // Pre-compute fixed quantities used each event
269 // -------------------------------------------------------------------------
270 const auto& mt = marley::MassTable::Instance();
271
272 double gs_mass = mt.get_atomic_mass( nucleus_pdg )
273 - net_charge * mt.get_particle_mass( marley_utils::ELECTRON );
274
275 double unbound_threshold = mt.unbound_threshold( nucleus_pdg );
276
277 int signal_proc_id = marley_hepmc3::get_nuhepmc_proc_id(
278 ProcType::StandaloneDecay );
279
280 // -------------------------------------------------------------------------
281 // Event loop
282 // -------------------------------------------------------------------------
283 // One-time warning flags for the level-snap behaviour
284 bool warned_snap = false; // suppress after first snap warning
285 bool warned_snap_sample = false; // suppress after first "will continue" note
286
287 for ( long evnum = 0; evnum < num_events; ++evnum ) {
288
289 // --- Sample Ex, twoJ, and parity for this event -----------------------
290 if ( sample_Ex ) {
291 Ex = gen.uniform_random_double( Ex_min, Ex_max, true );
292 }
293
294 size_t twoJ_index = gen.sample_from_distribution( twoJ_dist );
295 int twoJ = twoJ_vec.at( twoJ_index );
296
297 std::string par_str = parity_str;
298 if ( par_str == "random" ) {
299 size_t par_index = gen.sample_from_distribution( parity_dist );
300 par_str = parity_strings.at( par_index );
301 }
302 marley::Parity parity;
303 std::istringstream temp_iss( par_str );
304 temp_iss >> parity;
305
306 // --- Snap to nearest discrete level if below the unbound threshold ----
307 if ( Ex <= unbound_threshold ) {
308 auto* ds = gen.get_structure_db().get_decay_scheme( nucleus_pdg );
309 if ( ds ) {
310 auto* lev = ds->get_pointer_to_closest_level( Ex );
311
312 // Check what changes on the snap
313 double Ex_lev = lev->energy();
314 int twoJ_lev = lev->twoJ();
315 marley::Parity P_lev = lev->parity();
316
317 bool Ex_changed = ( std::abs(Ex_lev - Ex) > 1e-5 );
318 bool twoJ_changed = ( twoJ_lev != twoJ );
319 bool P_changed = ( static_cast<int>(P_lev)
320 != static_cast<int>(parity) );
321
322 if ( ( Ex_changed || twoJ_changed || P_changed ) && !warned_snap ) {
323 std::ostringstream warn_msg;
324 warn_msg << "User-specified nuclear state (Ex=" << Ex
325 << " MeV, 2J=" << twoJ
326 << ", P=" << parity
327 << ") was snapped to nearest discrete level (Ex="
328 << Ex_lev << " MeV, 2J=" << twoJ_lev
329 << ", P=" << P_lev << ").";
330 if ( !Ex_changed ) warn_msg << " Ex unchanged.";
331 if ( !twoJ_changed ) warn_msg << " 2J unchanged.";
332 if ( !P_changed ) warn_msg << " Parity unchanged.";
333 warn_msg << " This warning will not be repeated.";
334 MARLEY_LOG( WARN, "cmd.decay" ) << warn_msg.str();
335 warned_snap = true;
336
337 if ( sample_Ex && !warned_snap_sample ) {
338 MARLEY_LOG( WARN, "cmd.decay" ) << "Discrete level matching"
339 " will continue for subsequent events as Ex values below the"
340 " unbound threshold (" << unbound_threshold << " MeV) are"
341 " sampled. This message will not be repeated.";
342 warned_snap_sample = true;
343 }
344 }
345
346 // Apply the snap — use the level's quantum numbers
347 Ex = Ex_lev;
348 twoJ = twoJ_lev;
349 parity = P_lev;
350 }
351 }
352
353 // --- Build the HepMC3 event object ------------------------------------
354 //
355 // Primary vertex layout:
356 //
357 // IN: dummy projectile (PDG 0, status 4, zero 4-momentum)
358 // target nucleus (status 20, at rest, gs mass)
359 //
360 // OUT: dummy ejectile (PDG 0, status 1, zero 4-momentum)
361 // residue nucleus (status 27, mass = gs + Ex, with Ex/twoJ/parity
362 // attributes for NucleusDecayer)
363
364 auto event = std::make_shared< HepMC3::GenEvent >(
365 HepMC3::Units::MEV, HepMC3::Units::CM );
366
367 event->set_event_number( evnum + 1 );
368
369 event->add_attribute( "signal_process_id",
370 std::make_shared< HepMC3::IntAttribute >( signal_proc_id ) );
371
372 auto prim_vtx = std::make_shared< HepMC3::GenVertex >();
373 prim_vtx->set_status( marley_hepmc3::NUHEPMC_PRIMARY_VERTEX );
374 event->add_vertex( prim_vtx );
375
376 // Dummy projectile — PDG 0, zero 4-momentum
377 auto projectile = marley_hepmc3::make_particle( 0, 0., 0., 0., 0.,
378 marley_hepmc3::NUHEPMC_PROJECTILE_STATUS, 0. );
379
380 // Target nucleus — at rest, ground-state mass
381 auto target = marley_hepmc3::make_particle( nucleus_pdg,
382 marley_hepmc3::NUHEPMC_TARGET_STATUS, gs_mass );
383
384 // Dummy ejectile — clone of projectile (PDG 0, zero 4-momentum), final-state
385 auto ejectile = marley_hepmc3::make_particle( 0, 0., 0., 0., 0.,
386 marley_hepmc3::NUHEPMC_FINAL_STATE_STATUS, 0. );
387
388 // Residue nucleus — excited state
389 double m_residue = gs_mass + Ex;
390 auto residue = marley_hepmc3::make_particle( nucleus_pdg, 0., 0., 0.,
391 m_residue, marley_hepmc3::NUHEPMC_UNDECAYED_RESIDUE_STATUS, m_residue );
392
393 prim_vtx->add_particle_in( projectile );
394 prim_vtx->add_particle_in( target );
395 prim_vtx->add_particle_out( ejectile );
396 prim_vtx->add_particle_out( residue );
397
398 // Set attributes for the target + residue now that these particles
399 // are attached to the event
400 marley_hepmc3::set_particle_charge( *target, net_charge );
401 marley_hepmc3::set_particle_charge( *residue, net_charge );
402
403 residue->add_attribute( "Ex",
404 std::make_shared< HepMC3::DoubleAttribute >( Ex ) );
405 residue->add_attribute( "twoJ",
406 std::make_shared< HepMC3::IntAttribute >( twoJ ) );
407 residue->add_attribute( "parity",
408 std::make_shared< HepMC3::IntAttribute >( static_cast<int>(parity) ) );
409
410 // --- Run de-excitation cascade ----------------------------------------
412 nd.process_event( *event, gen );
413
414 gen.finish_event_metadata( *event );
415
416 for ( const auto& file : output_files ) {
417 file->write_event( event.get() );
418 }
419
420 std::cout << "Event " << evnum << "\n";
421 }
422
423 return true;
424}
static bool cmd_help(std::deque< std::string > &args)
Display top-level or command-specific help messages.
Definition cmd_help.cc:24
static bool cmd_decay(std::deque< std::string > &args)
Simulate nuclear de-excitations.
Definition cmd_decay.cc:60
Base class for all exceptions thrown by MARLEY functions.
Definition Error.hh:26
The MARLEY Event generator.
Definition Generator.hh:54
double uniform_random_double(double min, double max, bool inclusive)
Sample a random number uniformly on either [min, max) or [min, max].
Definition Generator.cc:235
void set_up_run_info()
Initializes the owned GenRunInfo object that will be used to associate run metadata with the output e...
Definition Generator.cc:769
marley::StructureDatabase & get_structure_db()
Get a reference to the StructureDatabase owned by this Generator.
Definition Generator.cc:510
auto sample_from_distribution(RandomNumberDistribution &rnd) -> decltype(std::declval< RandomNumberDistribution & >().operator()(std::declval< std::mt19937_64 & >()))
Sample from an arbitrary probability distribution (defined here as any object that implements an oper...
Definition Generator.hh:193
void finish_event_metadata(HepMC3::GenEvent &ev, bool attach_state=false)
Add final pieces of metadata (e.g., the RNG state) to an otherwise complete event.
Definition Generator.cc:827
static const MassTable & Instance()
Get a const reference to the singleton instance of the MassTable.
Definition MassTable.cc:69
EventProcessor that handles nuclear de-excitations.
virtual void process_event(HepMC3::GenEvent &event, marley::Generator &gen) override
Processes an input GenEvent object.
Type-safe representation of a parity value (either +1 or -1)
Definition Parity.hh:25
ProcessType
Enumerated type describing the kind of scattering process represented by a Reaction.
Definition Reaction.hh:58
marley::DecayScheme * get_decay_scheme(const int particle_id)
Retrieves discrete level data from the database.