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_generate.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 <chrono>
19#include <csignal>
20#include <cstdlib>
21#include <ctime>
22#include <iomanip>
23#include <iostream>
24#include <memory>
25#include <sstream>
26#include <string>
27#include <vector>
28
29// POSIX includes (available on Linux, macOS, and other Unix-like systems)
30#if __has_include(<sys/ioctl.h>)
31#include <sys/ioctl.h>
32#include <unistd.h>
33#endif
34
35// HepMC3 includes
36#include "HepMC3/GenEvent.h"
37
38// MARLEY includes
39#include "marley/CommandHandler.hh"
40#include "marley/Generator.hh"
41#include "marley/JSONConfig.hh"
42#include "marley/Logger.hh"
43#include "marley/OutputFile.hh"
44#include "marley/marley_utils.hh"
45#include "marley/hepmc3_utils.hh"
46
47namespace {
48
49 volatile static std::sig_atomic_t interrupted = false;
50 volatile static std::sig_atomic_t terminal_resized = false;
51
52 void signal_handler( int )
53 {
54 interrupted = true;
55 }
56
57 void sigwinch_handler( int )
58 {
59 terminal_resized = true;
60 }
61
62 constexpr int DEFAULT_STATUS_UPDATE_INTERVAL = 100;
63
64 // Files above this count are reported as a single combined total line.
65 // Adjust here; no other code needs to change.
66 constexpr size_t MAX_FILE_STATUS_LINES = 2u;
67
68 // Debounce period for terminal resize signals.
69 constexpr auto RESIZE_DEBOUNCE = std::chrono::milliseconds( 150 );
70
71 // True when the terminal is too small or stdout is not a TTY.
72 // Reset to false at the start of each cmd_generate() call.
73 static bool g_fallback_mode = false;
74
75 // True once setup_scroll_region() has been called and the ANSI scroll region
76 // is active. Used to distinguish the first call (which must scroll existing
77 // log content clear of the status zone) from subsequent resize calls (which
78 // must not inject spurious blank lines into the log zone).
79 // Reset to false at the start of each cmd_generate() call.
80 static bool g_status_region_active = false;
81
82 struct TermSize {
83 int rows;
84 int cols;
85 };
86
87 // Query terminal dimensions. On POSIX systems uses ioctl for accurate live
88 // size and TTY detection. Falls back to the COLUMNS and LINES environment
89 // variables (set by bash, zsh, etc.). Returns {0, 0} when no source is
90 // available, which callers treat as a signal to enter fallback mode and
91 // suppress escape sequences.
92 TermSize get_terminal_size() {
93#if __has_include(<sys/ioctl.h>)
94 // POSIX path: ioctl provides accurate live terminal dimensions and also
95 // detects whether stdout is a TTY (fails for pipes).
96 struct winsize w;
97 if ( ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == 0
98 && w.ws_row > 0 && w.ws_col > 0 )
99 {
100 return { static_cast< int >( w.ws_row ),
101 static_cast< int >( w.ws_col ) };
102 }
103#endif
104
105 // Env var fallback: COLUMNS and LINES are set by bash, zsh, and other
106 // shells for interactive terminal sessions.
107 const char* cols_str = std::getenv( "COLUMNS" );
108 const char* lines_str = std::getenv( "LINES" );
109 if ( cols_str != nullptr && lines_str != nullptr ) {
110 char* end = nullptr;
111 const long cols = std::strtol( cols_str, &end, 10 );
112 if ( end != cols_str && cols > 0 ) {
113 const long rows = std::strtol( lines_str, &end, 10 );
114 if ( end != lines_str && rows > 0 ) {
115 return { static_cast< int >( rows ),
116 static_cast< int >( cols ) };
117 }
118 }
119 }
120
121 return { 0, 0 };
122 }
123
124 // Truncate s to at most max_len visible characters, appending the ellipsis
125 // character (U+2026) if the string was cut. Counts bytes; acceptable because
126 // all status content is ASCII or near-ASCII. Substitute a UTF-8 column
127 // counter if non-ASCII filenames must be handled precisely.
128 std::string truncate_for_terminal( const std::string& s, int max_len ) {
129 if ( max_len <= 0 ) return {};
130 if ( static_cast< int >( s.size() ) <= max_len ) return s;
131 return s.substr( 0, (size_t)(max_len - 1) ) + "\xe2\x80\xa6"; // UTF-8 U+2026 …
132 }
133
134 std::string format_number( double number ) {
135 static std::stringstream temp_stream;
136 static bool configured = false;
137 if ( !configured ) {
138 temp_stream << std::fixed << std::setprecision(1);
139 configured = true;
140 }
141 temp_stream.str("");
142 temp_stream.clear();
143 temp_stream << number;
144 return temp_stream.str();
145 }
146
147 std::string put_time( std::tm* time, const char* format )
148 {
149 constexpr size_t TIME_STR_SIZE = 100;
150 std::string time_str( TIME_STR_SIZE, ' ' );
151 std::strftime( &time_str.front(), TIME_STR_SIZE, format, time );
152 marley_utils::trim_right_inplace( time_str );
153 return time_str;
154 }
155
156 // Declare (or re-declare) the scroll region and prepare the status zone.
157 // On the first call (no scroll region active yet), scrolls existing terminal
158 // content up by num_status_lines rows so that log output from before this
159 // call is not overwritten when the status zone is claimed and cleared.
160 // On subsequent calls (e.g. after a terminal resize), the scroll region is
161 // already active so no scroll-push is needed; only the status zone rows are
162 // cleared. Safe to call both at initial setup and after a terminal resize.
163 void setup_scroll_region( int num_status_lines ) {
164 TermSize ts = get_terminal_size();
165 int log_zone_end = ts.rows - num_status_lines;
166
167 if ( !g_status_region_active ) {
168 // No scroll region is active yet. Move to the very last row of the
169 // terminal and emit num_status_lines newlines. Because the cursor is at
170 // the bottom and no scroll region constrains the scroll, each newline
171 // scrolls the entire terminal up by one row, leaving exactly
172 // num_status_lines blank rows at the bottom for the status zone.
173 std::cout << "\033[" << ts.rows << ";1H"; // move to last row
174 for ( int i = 0; i < num_status_lines; ++i ) std::cout << '\n';
175 }
176
177 std::cout << "\033[1;" << log_zone_end << "r"; // declare scroll region
178
179 for ( int r = log_zone_end + 1; r <= ts.rows; ++r )
180 std::cout << "\033[" << r << ";1H\033[K"; // clear status zone only
181
182 std::cout << "\033[" << log_zone_end << ";1H"; // park cursor at base of log zone
183 std::flush( std::cout );
184
185 g_status_region_active = true;
186 }
187
188 // Reset scroll region on every exit path.
189 //
190 // When num_status_lines > 0 and clear_status == true (normal / SIGINT exit):
191 // clears the status zone rows, resets the scroll region, and positions the
192 // cursor immediately below the last log content so the final summary prints
193 // without a gap.
194 //
195 // When num_status_lines > 0 and clear_status == false (exception exit):
196 // leaves the status zone intact so the user can see progress at the time of
197 // the throw, resets the scroll region, and positions the cursor just below
198 // the last status row so the error message appears directly beneath it.
199 //
200 // When num_status_lines == 0 (early exception or fallback mode):
201 // just resets the scroll region and leaves the cursor in place.
202 void reset_terminal( int num_status_lines = 0, bool clear_status = true ) {
203 TermSize ts = get_terminal_size();
204 if ( ts.rows == 0 ) return; // non-TTY: no escape codes needed
205
206 if ( num_status_lines > 0 ) {
207 int log_zone_end = ts.rows - num_status_lines;
208 if ( clear_status ) {
209 // Clear status zone rows so they don't linger as blank lines
210 for ( int r = log_zone_end + 1; r <= ts.rows; ++r )
211 std::cout << "\033[" << r << ";1H\033[K";
212 // Reset scroll region, position cursor right below last log content
213 std::cout << "\033[r"
214 << "\033[" << log_zone_end << ";1H";
215 } else {
216 // Preserve status zone; position cursor just below it
217 std::cout << "\033[r"
218 << "\033[" << ts.rows << ";1H\n";
219 }
220 } else {
221 std::cout << "\033[r"; // reset scroll region; leave cursor in place
222 }
223 std::flush( std::cout );
224 g_status_region_active = false;
225 }
226
227 // Switch to plain streaming output with no status display. Safe to call when
228 // stdout is a pipe (ts.rows == 0); the notice is suppressed in that case.
229 void enter_fallback_mode() {
230 if ( g_fallback_mode ) return;
231 g_fallback_mode = true;
232 g_status_region_active = false;
233 TermSize ts = get_terminal_size();
234 if ( ts.rows > 0 ) {
235 std::cout << "\033[r" // reset scroll region (no-op if never set)
236 << "[MARLEY] Terminal too small for status display."
237 " Running in log-only mode.\n";
238 }
239 std::flush( std::cout );
240 }
241
242 // Leave fallback mode and reinitialize the scroll region. Called when the
243 // terminal is resized to a usable size while fallback mode is active.
244 void exit_fallback_mode( int num_status_lines ) {
245 if ( !g_fallback_mode ) return;
246 g_fallback_mode = false;
247 setup_scroll_region( num_status_lines );
248 std::cout << "[MARLEY] Terminal size restored. Status display active.\n";
249 std::flush( std::cout );
250 }
251
252 // Overwrite the status zone using absolute cursor positioning, then restore
253 // the cursor to its saved position in the log zone. Never touches the log
254 // zone during rendering; no line-counting required.
255 void update_status_bars(
256 long ev_count, long num_events, long num_old_events,
257 std::chrono::system_clock::time_point start_time_point,
258 const std::vector< std::shared_ptr<marley::OutputFile> >& output_files,
259 int num_status_lines )
260 {
261 TermSize ts = get_terminal_size();
262 int status_start_row = ts.rows - num_status_lines + 1;
263
264 // Compute display values (same arithmetic as the original makeStatusLines())
265 auto current_tp = std::chrono::system_clock::now();
266
267 auto elapsed = std::chrono::duration_cast< marley_utils::seconds<double> >(
268 current_tp - start_time_point );
269 double avg_rate = (ev_count - num_old_events) / elapsed.count();
270 double pct = static_cast<double>( ev_count ) / num_events * 100.;
271
272 marley_utils::seconds<double> est_total =
273 ( current_tp - start_time_point )
274 * ( static_cast<double>(num_events - num_old_events)
275 / (ev_count - num_old_events) );
276
277 std::time_t est_end = std::chrono::system_clock::to_time_t(
278 start_time_point + std::chrono::duration_cast<
279 std::chrono::system_clock::duration>( est_total ) );
280
281 std::ostringstream oss;
282 oss << "\033[s"; // save cursor (current log zone position)
283
284 // Jump to absolute row, clear it, write truncated content.
285 auto write_line = [&]( int row, const std::string& content ) {
286 oss << "\033[" << row << ";1H\033[K";
287 oss << truncate_for_terminal( content, ts.cols );
288 };
289
290 int row = status_start_row;
291
292 // Line 1: event count and rate
293 { std::ostringstream l;
294 l << "Event Count = " << ev_count << "/" << num_events
295 << " (" << format_number(pct) << "% complete, "
296 << format_number(avg_rate) << " events / s)";
297 write_line( row++, l.str() ); }
298
299 // Line 2: elapsed and estimated total run time
300 { std::ostringstream l;
301 l << "Elapsed time: "
302 << marley_utils::elapsed_time_string(start_time_point, current_tp)
303 << " (Estimated total run time: "
304 << marley_utils::duration_to_string< marley_utils::seconds<double> >(
305 est_total ) << ")";
306 write_line( row++, l.str() ); }
307
308 // Line(s) 3+: output file status (individual or combined)
309 if ( output_files.size() <= MAX_FILE_STATUS_LINES ) {
310 for ( const auto& file : output_files ) {
311 std::ostringstream l;
312 l << "Data written to " << file->name() << " "
313 << marley_utils::num_bytes_to_string( file->bytes_written(), 2 )
314 << " (estimate)";
315 write_line( row++, l.str() );
316 }
317 } else {
318 size_t total_bytes = 0;
319 for ( const auto& file : output_files )
320 total_bytes += file->bytes_written();
321 std::ostringstream l;
322 l << "Data written to " << output_files.size() << " output files "
323 << marley_utils::num_bytes_to_string( total_bytes, 2 ) << " (estimate)";
324 write_line( row++, l.str() );
325 }
326
327 // Last line: estimated termination timestamp
328 { std::ostringstream l;
329 l << "MARLEY is estimated to terminate on "
330 << put_time( std::localtime(&est_end), "%c %Z" );
331 write_line( row, l.str() ); }
332
333 oss << "\033[u"; // restore cursor to log zone
334 std::cout << oss.str();
335 std::flush( std::cout );
336 }
337
338} // anonymous namespace
339
340bool marley::CommandHandler::cmd_generate( std::deque< std::string >& args ) {
341
342 // Declared here so the catch block can pass it to reset_terminal().
343 // Remains 0 if an exception is thrown before output_files is populated.
344 int num_status_lines = 0;
345
346 // Flag that indicates whether caught exceptions need to call
347 // reset_terminal(). After we set up the status line display, this becomes
348 // important. Before that, it clears logging messages from the screen
349 // unnecessarily.
350 bool exception_needs_reset_terminal = false;
351
352 // Create a pointer to store a buffered event and its corresponding
353 // random number generator state string. We write out the buffered
354 // event only when generation of the following event has successfully
355 // completed. This saves space by allowing the generator state to be
356 // saved only when needed at the end of a run (normal termination or
357 // an early exit in response to an interruption/exception). The state can
358 // be used to restart the MARLEY simulation job where it left off
359 // without a drift in the random number state.
360 std::shared_ptr< HepMC3::GenEvent > buffered_event;
361 std::string cached_generator_state;
362
363 std::vector< std::shared_ptr<marley::OutputFile> > output_files;
364
365 try {
366
367 std::string config_file_name;
368 if ( !args.empty() ) config_file_name = args.front();
369
370 bool cfn_empty = config_file_name.empty();
371
372 if ( cfn_empty || config_file_name.front() == '-' )
373 {
374 if ( !cfn_empty && config_file_name != "-h"
375 && config_file_name != "--help" )
376 {
377 std::cerr << "marley generate: unrecognized option '"
378 << config_file_name << "'\n";
379 }
380 args.clear();
381 args.push_front( "generate" );
383 return false;
384 }
385
386 marley::JSON json = marley::JSON::load_file( config_file_name );
387 marley::JSONConfig jc( json );
388
389 std::chrono::system_clock::time_point start_time_point
390 = std::chrono::system_clock::now();
391
392 std::time_t start_time = std::chrono::system_clock::to_time_t(
393 start_time_point );
394
395 std::cout << "\nMARLEY started on "
396 << put_time( std::localtime(&start_time), "%c %Z" ) << '\n';
397
398 long num_old_events = 0;
399
400 marley::JSON ex_set = json.get_object( "generate", false );
401 long num_events = ex_set.get_long( "events", 1e3 );
402
403 MARLEY_LOG( INFO, "app" ) << "Requested events: " << num_events
404 << ", configuration file: \"" << config_file_name << "\"";
405
406 int status_update_interval = DEFAULT_STATUS_UPDATE_INTERVAL;
407 if ( ex_set.has_key("status_update_interval") ) {
408 const auto& sui = ex_set.at( "status_update_interval" );
409 bool ok;
410 int sui_value = sui.to_long( ok );
411
412 if ( !ok || sui_value < 1 ) {
413 throw marley::Error( "Invalid value " + sui.dump_string()
414 + " given for the \"status_update_interval\" key in the"
415 " job configuration file" );
416 }
417 else status_update_interval = sui_value;
418 }
419
420 if ( ex_set.has_key("output") ) {
421 marley::JSON output_set = ex_set.at( "output" );
422 if ( !output_set.is_array() ) throw marley::Error( "The"
423 " \"output\" key in the \"generate\" section must have a value that"
424 " is a JSON array." );
425 else for ( const auto& el : output_set.array_range() ) {
426 output_files.push_back( marley::OutputFile::make_OutputFile(el) );
427 }
428 }
429 else {
430 std::string out_config_str = "{ format: \"ascii\","
431 " file: \"events.hepmc3\", mode: \"overwrite\", force: false }";
432 auto out_config = marley::JSON::load( out_config_str );
433
434 output_files.push_back( marley::OutputFile::make_OutputFile(
435 out_config ) );
436 }
437
438 // Fixed status line count for this run (computed once; used for scroll
439 // region sizing and fallback threshold throughout).
440 const int file_lines = ( output_files.size() <= MAX_FILE_STATUS_LINES )
441 ? static_cast< int >( output_files.size() ) : 1;
442 num_status_lines = 3 + file_lines;
443
444 // Reset module-level state in case cmd_generate is called more than once.
445 interrupted = false;
446 terminal_resized = false;
447 g_fallback_mode = false;
448 g_status_region_active = false;
449
450 std::signal( SIGINT, signal_handler );
451 std::signal( SIGWINCH, sigwinch_handler );
452
453 // Create the generator before setting up the scroll region so that
454 // initialization logging (including the active-reaction summary printed
455 // at the end of create_generator()) appears before the status zone is
456 // claimed. This avoids truncation of those log messages.
457 std::unique_ptr< marley::Generator > gen;
458
459 bool need_to_resume = false;
460 for ( auto& file : output_files ) {
461 if ( file->mode_is_resume() ) {
462 if ( need_to_resume ) throw marley::Error( "Only one file may be used"
463 " to resume a previous run." );
464 else {
465 need_to_resume = true;
466 bool resume_ok = file->resume( gen, num_old_events );
467 if ( !resume_ok ) throw marley::Error( "Failed to resume previous"
468 " run from the file \"" + file->name() + '\"' );
469 }
470 }
471 }
472
473 if ( !need_to_resume ) gen = std::make_unique<marley::Generator>(
474 jc.create_generator() );
475
476 // Reset the start timestamp after generator creation so that rate and ETA
477 // calculations exclude initialization overhead.
478 start_time_point = std::chrono::system_clock::now();
479 start_time = std::chrono::system_clock::to_time_t( start_time_point );
480
481 exception_needs_reset_terminal = true;
482
483 // Initialize the terminal display now that generator construction is done.
484 // Enter fallback mode if stdout is not a TTY or the terminal is too small.
485 {
486 TermSize ts = get_terminal_size();
487 if ( ts.rows == 0 || ts.rows < num_status_lines + 2 ) {
488 enter_fallback_mode();
489 } else {
490 setup_scroll_region( num_status_lines );
491 }
492 }
493
494 long ev_count = 1 + num_old_events;
495
496 auto last_resize_time = std::chrono::steady_clock::time_point{};
497
498 for (; ev_count <= num_events && !interrupted; ++ev_count) {
499
500 // Handle terminal resize with clock-based debounce. The debounce
501 // prevents thrashing during active window dragging; it is included
502 // unconditionally because some physics configurations (e.g. CEvNS)
503 // run at rates too fast to rely on event-loop latency alone.
504 if ( terminal_resized ) {
505 auto now = std::chrono::steady_clock::now();
506 if ( now - last_resize_time >= RESIZE_DEBOUNCE ) {
507 terminal_resized = false;
508 last_resize_time = now;
509
510 TermSize ts = get_terminal_size();
511 if ( ts.rows == 0 || ts.rows < num_status_lines + 2 ) {
512 enter_fallback_mode();
513 } else if ( g_fallback_mode ) {
514 exit_fallback_mode( num_status_lines );
515 } else {
516 setup_scroll_region( num_status_lines );
517 }
518 }
519 }
520
521 auto event = gen->create_event();
522 event->set_event_number( ev_count );
523
524 // If we have a buffered event from the previous iteration, we will
525 // write it to the output file(s) now. The generator state string
526 // will not be included here to save space. We can use implicit
527 // conversion of the std::shared_ptr here since it default-constructs
528 // to a nullptr and will therefore evaluate to false if we haven't
529 // used it yet.
530 if ( buffered_event ) {
531 for ( const auto& file : output_files ) {
532 file->write_event( buffered_event.get() );
533 }
534 }
535
536 // Replace the buffered event with the current event. Also cache the
537 // generator state string value corresponding to when the current
538 // event was finished.
539 buffered_event = event;
540 cached_generator_state = gen->get_state_string();
541
542 if ( !g_fallback_mode
543 && ( (ev_count - num_old_events) % status_update_interval == 1
544 || ev_count == num_events
545 || status_update_interval == 1 ) )
546 {
547 update_status_bars( ev_count, num_events, num_old_events,
548 start_time_point, output_files, num_status_lines );
549 }
550
551 } // event loop
552
553 // We've exited the event loop, so write out the last completed event
554 // (if any), which will be stored in the buffered_event pointer.
555 // Attach the generator state string this time so that the MARLEY
556 // job can be resumed from where it left off.
557 // NOTE: This call to OutputFile::write_event() handles normal
558 // termination and interruption via the SIGINT signal. The exception
559 // exit path is handled separately below in the catch block.
560 if ( buffered_event ) {
562 cached_generator_state );
563 for ( const auto& file : output_files ) {
564 file->write_event( buffered_event.get() );
565 }
566 }
567
568 reset_terminal( g_fallback_mode ? 0 : num_status_lines );
569
570 for ( const auto& file : output_files ) {
571 std::cout << "Data written to " << file->name() << ' '
572 << marley_utils::num_bytes_to_string( file->bytes_written() ) << '\n';
573 }
574
575 std::chrono::system_clock::time_point end_time_point
576 = std::chrono::system_clock::now();
577 std::time_t end_time
578 = std::chrono::system_clock::to_time_t( end_time_point );
579
580 if ( !interrupted ) {
581 MARLEY_LOG( NOTICE, "app" ) << "Generated " << ( ev_count - 1
582 - num_old_events ) << " event(s) successfully";
583 std::cout << "MARLEY terminated normally on ";
584 }
585 else {
586 MARLEY_LOG( NOTICE, "app" ) << "Generation interrupted after "
587 << ( ev_count - 1 - num_old_events ) << " event(s)";
588 std::cout << "MARLEY was interrupted by the user on ";
589 }
590 std::cout << put_time( std::localtime(&end_time), "%c %Z" ) << '\n';
591
592 return true;
593 }
594
595 catch ( const std::exception& error ) {
596 if ( exception_needs_reset_terminal && g_status_region_active ) {
597 reset_terminal( g_fallback_mode ? 0 : num_status_lines,
598 /*clear_status=*/false );
599 }
600
601 // Write out the buffered event (if any) that was successfully completed
602 // before the exception occurred. Attach the generator state string
603 // so that the MARLEY job can be restarted from the last successful
604 // event for easier debugging.
605 // NOTE: The buffered event was fully created before the exception
606 // occurred, so no exceptions are expected to be thrown in this block.
607 // Just in case, we wrap it with an additional try/catch to inform
608 // the user if writing out the buffered event fails.
609 try {
610 if ( buffered_event ) {
612 cached_generator_state );
613 for ( const auto& file : output_files ) {
614 file->write_event( buffered_event.get() );
615 }
616 }
617 } catch ( const std::exception& except ) {
618 MARLEY_LOG( WARN, "app" ) << std::flush << "Output of buffered"
619 " MARLEY event and its generator state failed";
620 MARLEY_LOG( ERROR, "app" ) << except.what();
621 }
622
623 MARLEY_LOG( ERROR, "app" ) << std::flush << error.what();
624 }
625
626 return false;
627}
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_generate(std::deque< std::string > &args)
Generate Monte Carlo events.
Base class for all exceptions thrown by MARLEY functions.
Definition Error.hh:26
void add_state_to_event(HepMC3::GenEvent &ev) const
Attach the current random number generator state to the input event as a string attribute.
Definition Generator.cc:880