Quantum 3xplorer (Q3)
An AI-native quantum simulation engine
Loading...
Searching...
No Matches
cli.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * Shared CLI helpers for the q3 and q3_mpi entry points.
5 *
6 * Parsing does NOT apply side effects (e.g. log level): it only records the
7 * requested values. Entry points apply them after their logger is initialized,
8 * so that a later log::init() / log::init_mpi() cannot silently discard a
9 * --log-level request.
10 */
11
14#include "io/logger.hpp"
17#include "utils/time.hpp"
18
19#include <cstdlib>
20#include <string>
21
22namespace q3 {
23namespace cli {
24
25/**
26 * One line naming the build: version, and the commit if the build supplied one.
27 *
28 * Assembled here rather than at each entry point so the five binaries cannot
29 * drift into reporting themselves differently. The commit is empty for a build
30 * from a dirty tree or from no repository at all -- see version.hpp for why
31 * that is reported as unknown rather than guessed.
32 */
33inline std::string build_identity(const std::string& binary_name) {
34 std::string identity = binary_name + " " + std::string(q3::version);
35 if (std::string(q3::commit).empty()) {
36 return identity + " (commit unknown: built from a modified tree or without git)";
37 }
38 return identity + " (" + std::string(q3::commit) + ")";
39}
40
41/**
42 * Print what `--version` reports: the build identity on its own first line,
43 * which scripts parse, then the add-ons compiled in, when there are any.
44 */
45inline void print_version(const std::string& binary_name) {
46 log::info("{}", build_identity(binary_name));
47 if (active_addons().empty())
48 return;
49 std::string addons;
50 for (const AddonIdentity& addon : active_addons())
51 addons += (addons.empty() ? "" : ", ") + std::string(addon.name) + " " + addon.version;
52 log::info("add-ons: {}", addons);
53}
54
55/// The `--format` values every registered input format accepts, as usage text: `<yaml|qe|...>`.
56inline std::string input_format_choices() {
57 return "<" + input_format_registry().canonical_names("|") + ">";
58}
59
60/// What a usage line says an input path may name: every registered format, in order.
61inline std::string input_format_descriptions() {
62 const auto& formats = input_format_registry().entries();
63 std::string descriptions;
64 for (std::size_t index = 0; index < formats.size(); index++) {
65 if (index > 0)
66 descriptions += index + 1 == formats.size() ? ", or " : ", ";
67 descriptions += formats[index]->display_name();
68 }
69 return descriptions;
70}
71
72struct Options {
73 /**
74 * The input path. Empty when none was given, which is not an error when a
75 * registered input format claims the working directory: some formats are
76 * run with no arguments from the directory holding the input. The entry
77 * point decides what an empty value means.
78 */
79 std::string input_file;
80 /**
81 * Explicit input format from `--format`, e.g. "qe"; empty means detect.
82 *
83 * Detection is by file name, extension or directory contents, so an input
84 * renamed by a workflow tool, or one in a directory named something
85 * unexpected, needs a way to say what it is.
86 */
87 std::string format;
88 /// Requested log level ("trace" ... "off"); empty means keep the default.
89 std::string log_level;
90 /// Number of k-point pools (MPI binary only). 0 = execution policy default.
92 /// Whether `--pools` was present, so an input-file value can be honored otherwise.
94 /**
95 * Divide the bands of a pool across its ranks instead of its reciprocal-space
96 * coefficients (MPI binary only). Each rank then keeps whole coefficient
97 * vectors, so its transforms are local and serial, and gamma-only, stress and
98 * export stay available -- all of which the slab split gives up.
99 */
100 bool distribute_bands = false;
101 /**
102 * Requested precision mode (GPU binaries only): "double" or "mixed".
103 * Empty means the binary's default. Validated by the GPU entry point.
104 */
105 std::string precision;
106 /**
107 * `--check`: read the input, report what it resolved to, and exit without
108 * running anything. For a drop-in replacement the first question a user has
109 * about someone else's deck is "will this even be read, and as what?", and
110 * answering it should not cost an SCF.
111 */
112 bool check_only = false;
113 /**
114 * `--log-file <path>`: also write the log to a file. Empty means console
115 * only. Under MPI each non-root rank writes to `<path>.<rank>`, since the
116 * console carries rank 0 alone and a rank that fails on its own has
117 * nowhere else to say so.
118 */
119 std::string log_file;
120 /**
121 * `--version`: print the build identity and exit. Separate from the run
122 * because the answer must not depend on having a valid input: the question
123 * is asked precisely when a result is in doubt, and by scripts recording
124 * what produced one.
125 */
126 bool print_version = false;
127 /**
128 * `--material <file>`: run the input as one geometry step of a relaxation,
129 * an SCF at the cell and positions of this ESSE material (see
130 * run_at_material). How q3.workflows relaxes any input on any binary.
131 */
132 std::string material_file;
133};
134
135/**
136 * The usage every binary prints: one text, so the binaries cannot drift apart.
137 * A flag a binary has no use for is read and ignored there, as documented in
138 * docs/io_reference.md.
139 */
140inline void print_usage(const std::string& binary_name) {
141 log::error("Usage: {} [input] [--format {}] [--material <file>] [--check]",
142 binary_name,
144 log::error(" [--pools <n>] [--distribute-bands] [--precision double|mixed]");
145 log::error(" [--log-level <level>] [--log-file <path>] [--version]");
147 log::error(" --material <file> run the input at this ESSE structure (cell, positions)");
148 log::error(" --check read the input, report it, and run nothing");
149 log::error(" --pools <n> k-point pools (MPI binaries)");
150 log::error(" --distribute-bands split bands, not reciprocal space, in a pool (MPI)");
151 log::error(" --precision <mode> double or mixed (GPU binaries)");
152 log::error(" --log-file <path> also log to a file (MPI rank N writes <path>.N)");
153 log::error(" --version print the build identity and exit");
154}
155
156/**
157 * Parse command-line options shared by the q3, q3_mpi, and q3_gpu* binaries.
158 *
159 * Recognized flags: --log-level <level>, --log-file <path>, --pools <n>, --distribute-bands,
160 * --precision <mode>, --format <name>, --material <file>, --check, --version. The input path is
161 * the first argument that is not a flag or
162 * a flag's value, so it may appear before or after the flags — `q3 --format
163 * qe scf.in` and `q3 scf.in --format qe` both work, and so does giving no path
164 * at all.
165 *
166 * Leaving the path empty is **not** an error here. Only the entry point knows
167 * whether an argument-less invocation is meaningful: it is when a registered
168 * input format claims the working directory (resolve_argument_less_input()).
169 *
170 * @param argc Argument count from main().
171 * @param argv Argument vector from main().
172 * @param binary_name Named in the usage text, printed and followed by exit(1)
173 * only for a malformed flag.
174 */
175inline Options parse_options(int argc, char* argv[], const std::string& binary_name) {
176 Options options;
177
178 for (int argument_index = 1; argument_index < argc; ++argument_index) {
179 std::string argument(argv[argument_index]);
180
181 auto take_value = [&](const char* flag_name) -> std::string {
182 if (argument_index + 1 >= argc) {
183 log::error("{} needs a value", flag_name);
184 print_usage(binary_name);
185 std::exit(1);
186 }
187 return argv[++argument_index];
188 };
189
190 if (argument == "--log-level") {
191 options.log_level = take_value("--log-level");
192 } else if (argument == "--log-file") {
193 options.log_file = take_value("--log-file");
194 } else if (argument == "--version" || argument == "-v") {
195 options.print_version = true;
196 } else if (argument == "--pools") {
197 options.number_of_kpoint_pools = std::stoi(take_value("--pools"));
198 options.number_of_kpoint_pools_was_set = true;
199 } else if (argument == "--distribute-bands") {
200 options.distribute_bands = true;
201 } else if (argument == "--precision") {
202 options.precision = take_value("--precision");
203 } else if (argument == "--format") {
204 options.format = take_value("--format");
205 } else if (argument == "--material") {
206 options.material_file = take_value("--material");
207 } else if (argument == "--check") {
208 options.check_only = true;
209 } else if (argument.rfind("--", 0) == 0) {
210 /**
211 * Refused rather than ignored. Silently swallowing it made
212 * `q3 --typo deck/` take "--typo" as the input path and discard
213 * the directory, which fails much further from the cause.
214 */
215 log::error("Unrecognized option {}", argument);
216 print_usage(binary_name);
217 std::exit(1);
218 } else if (options.input_file.empty()) {
219 options.input_file = argument;
220 } else {
222 "Unexpected extra argument {} (input is already {})", argument, options.input_file);
223 print_usage(binary_name);
224 std::exit(1);
225 }
226 }
227
228 return options;
229}
230
231/**
232 * Read the input every binary runs, the same way in each: resolve an omitted
233 * path (the working directory, when a registered input format claims it),
234 * parse it, substitute `--material`'s structure, and apply the input's
235 * verbosity.
236 *
237 * Throws, after printing the usage, when no input was given and none is found.
238 */
239inline InputData read_input(Options& options, const std::string& binary_name) {
240 if (options.input_file.empty()) {
241 const InputFormat* claiming_format = format_claiming_directory(".");
242 if (claiming_format == nullptr) {
243 print_usage(binary_name);
244 throw std::runtime_error(
245 "no input given, and no input format claims the working directory");
246 }
247 options.input_file = ".";
248 log::info("No input given; found {} in the current directory",
249 claiming_format->display_name());
250 }
251 InputData input = parse_input(options.input_file, options.format);
252 if (!options.material_file.empty())
253 substitute_material(input, options.material_file);
255 return input;
256}
257
258/**
259 * Print an 80-column banner with a centered, timestamped label.
260 */
261inline void print_timestamped_banner(const std::string& label_prefix) {
262 std::string divider_line(80, '=');
263 std::string label = label_prefix + get_iso_timestamp_with_timezone();
264 int left_padding = (78 - static_cast<int>(label.length())) / 2;
265 int right_padding = 78 - static_cast<int>(label.length()) - left_padding;
266 std::string banner_line =
267 "=" + std::string(left_padding, ' ') + label + std::string(right_padding, ' ') + "=";
268
269 log::info("{}", divider_line);
270 log::info("{}", banner_line);
271 log::info("{}", divider_line);
272}
273
274inline void print_header() {
275 print_timestamped_banner("BEGIN: ");
276}
277
278inline void print_footer() {
280}
281
282} // namespace cli
283} // namespace q3
One input format a run can be read from; see the file comment.
virtual std::string display_name() const =0
How a log line names an input of this format, e.g. "a Quantum ESPRESSO input".
std::string input_format_choices()
The --format values every registered input format accepts, as usage text: <yaml|qe|....
Definition cli.hpp:56
std::string input_format_descriptions()
What a usage line says an input path may name: every registered format, in order.
Definition cli.hpp:61
std::string build_identity(const std::string &binary_name)
Definition cli.hpp:33
Options parse_options(int argc, char *argv[], const std::string &binary_name)
Definition cli.hpp:175
void print_version(const std::string &binary_name)
Definition cli.hpp:45
InputData read_input(Options &options, const std::string &binary_name)
Definition cli.hpp:239
void print_footer()
Definition cli.hpp:278
void print_timestamped_banner(const std::string &label_prefix)
Definition cli.hpp:261
void print_usage(const std::string &binary_name)
Definition cli.hpp:140
void print_header()
Definition cli.hpp:274
void error(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:192
void info(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:183
void apply_verbosity(const std::string &verbosity, const std::string &explicit_log_level)
Definition logger.hpp:113
const std::vector< AddonIdentity > & active_addons()
The add-ons compiled into this build, in the order the build discovered them.
constexpr const char * commit
Definition version.hpp:31
constexpr const char * version
Definition version.hpp:26
void substitute_material(InputData &input, const std::string &material_file)
Definition factory.hpp:83
InputData parse_input(const std::string &filename, const std::string &format="")
Definition factory.hpp:122
const InputFormat * format_claiming_directory(const std::filesystem::path &directory)
std::string get_iso_timestamp_with_timezone()
Definition time.hpp:21
Registry< InputFormat > & input_format_registry()
The registry every entry point reads input formats from.
One add-on compiled into this build.
Parsed input data — holds raw values read from input files.
Definition data.hpp:27
std::string verbosity
Definition data.hpp:186
std::string precision
Definition cli.hpp:105
int number_of_kpoint_pools
Number of k-point pools (MPI binary only). 0 = execution policy default.
Definition cli.hpp:91
bool print_version
Definition cli.hpp:126
bool number_of_kpoint_pools_was_set
Whether --pools was present, so an input-file value can be honored otherwise.
Definition cli.hpp:93
std::string log_file
Definition cli.hpp:119
bool distribute_bands
Definition cli.hpp:100
std::string material_file
Definition cli.hpp:132
std::string format
Definition cli.hpp:87
std::string log_level
Requested log level ("trace" ... "off"); empty means keep the default.
Definition cli.hpp:89
std::string input_file
Definition cli.hpp:79
bool check_only
Definition cli.hpp:112