TkN 2.7
Toolkit for Nuclei
Loading...
Searching...
No Matches
tkmoments_builder.cpp
1/********************************************************************************
2 * Copyright (c) : Université de Lyon 1, CNRS/IN2P3, UMR5822, *
3 * IP2I, F-69622 Villeurbanne Cedex, France *
4 * *
5 * Licensed under the MIT License <http://opensource.org/licenses/MIT>. *
6 * SPDX-License-Identifier: MIT *
7 ********************************************************************************/
8
9#include "tkmoments_builder.h"
10
11#include "json.hpp"
12#include "tkcsv.h"
13#include "tklog.h"
14
15#include <algorithm>
16#include <array>
17#include <cctype>
18#include <cstdlib>
19#include <cmath>
20#include <filesystem>
21#include <fstream>
22#include <iterator>
23#include <regex>
24#include <set>
25#include <sstream>
26
27using namespace tkn;
28using json = nlohmann::json;
29
30namespace {
31
32std::string trim(const std::string &value)
33{
34 const auto first = value.find_first_not_of(" \t\r\n");
35 if (first == std::string::npos) return "";
36 const auto last = value.find_last_not_of(" \t\r\n");
37 return value.substr(first, last - first + 1);
38}
39
40void remove_spaces(std::string &value)
41{
42 value.erase(
43 std::remove_if(value.begin(), value.end(), [](unsigned char character) {
44 return std::isspace(character) != 0;
45 }),
46 value.end());
47}
48
49void append_tag(std::string &info, const std::string &tag)
50{
51 if (tag.empty()) return;
52 info += ";" + tag;
53}
54
55double uncertainty_from_digits(const std::string &digits, const std::string &central)
56{
57 if (digits.find('.') != std::string::npos) return std::stod(digits);
58 const auto decimal = central.find('.');
59 const int decimal_places = decimal == std::string::npos
60 ? 0
61 : static_cast<int>(central.size() - decimal - 1);
62 return std::stod(digits) * std::pow(10., -decimal_places);
63}
64
65std::string column_value(const std::vector<std::string> &row,
66 const std::map<std::string, std::size_t> &columns,
67 const std::string &name)
68{
69 const auto position = columns.find(name);
70 if (position == columns.end() || position->second >= row.size()) return "";
71 return trim(row[position->second]);
72}
73
74std::string sqlite_text(sqlite3_stmt *statement, int column)
75{
76 const auto *value = sqlite3_column_text(statement, column);
77 return value ? reinterpret_cast<const char *>(value) : "";
78}
79
80struct spin_value {
81 int numerator = 0;
82 int denominator = 1;
83};
84
85std::string normalized_spin(std::string value)
86{
87 value = trim(value);
88 value.erase(
89 std::remove_if(value.begin(), value.end(), [](unsigned char character) {
90 return std::isspace(character) || character == '(' || character == ')' ||
91 character == '[' || character == ']';
92 }),
93 value.end());
94 std::transform(value.begin(), value.end(), value.begin(), [](unsigned char character) {
95 return static_cast<char>(std::tolower(character));
96 });
97
98 return value;
99}
100
101bool has_reversed_parity_notation(const std::string &text)
102{
103 return std::regex_match(trim(text), std::regex(R"(^-\d+(?:/\d+)?$)"));
104}
105
106bool set_column_value(std::vector<std::string> &row,
107 const std::map<std::string, std::size_t> &columns,
108 const std::string &name,
109 const std::string &value)
110{
111 const auto position = columns.find(name);
112 if (position == columns.end() || position->second >= row.size()) return false;
113 row[position->second] = value;
114 return true;
115}
116
117bool repair_reversed_parity_notation(
118 std::vector<std::string> &row,
119 const std::map<std::string, std::size_t> &columns)
120{
121 const std::string spin = column_value(row, columns, "spin");
122 std::smatch reversed_parity;
123 if (!std::regex_match(spin, reversed_parity,
124 std::regex(R"(^-(\d+(?:/\d+)?)$)")))
125 return false;
126 return set_column_value(row, columns, "spin", reversed_parity[1].str() + "-");
127}
128
129bool apply_source_correction(std::vector<std::string> &row,
130 const std::map<std::string, std::size_t> &columns,
131 const std::string &source_filename,
132 const std::string &moment_column,
133 const json &corrections,
134 std::string &source_reference)
135{
136 if (!corrections.is_array()) return false;
137 const std::string source_basename =
138 std::filesystem::path(source_filename).filename().string();
139 for (const auto &correction : corrections) {
140 if (!correction.is_object() ||
141 correction.value("file", std::string()) != source_basename ||
142 correction.value("moment_column", std::string()) != moment_column ||
143 !correction.contains("match") || !correction["match"].is_object() ||
144 !correction.contains("replace") || !correction["replace"].is_object())
145 continue;
146
147 auto match_fields = correction["match"].items();
148 const bool matches = std::all_of(
149 match_fields.begin(), match_fields.end(),
150 [&row, &columns](const auto &field) {
151 return field.value().is_string() &&
152 column_value(row, columns, field.key()) == field.value().template get<std::string>();
153 });
154 if (!matches) continue;
155
156 std::vector<std::string> repaired = row;
157 auto replacement_fields = correction["replace"].items();
158 if (!std::all_of(
159 replacement_fields.begin(), replacement_fields.end(),
160 [&repaired, &columns](const auto &field) {
161 return field.value().is_string() &&
162 set_column_value(repaired, columns, field.key(),
163 field.value().template get<std::string>());
164 }))
165 return false;
166 row = std::move(repaired);
167 source_reference = correction.value("source_reference", std::string());
168 return true;
169 }
170 return false;
171}
172
173bool load_source_corrections(const char *filename,
174 json &corrections,
175 std::string &reason)
176{
177 corrections = json::array();
178 if (!filename || !filename[0]) return true;
179 std::ifstream input(filename);
180 if (!input.good()) {
181 reason = std::string("cannot open ") + filename;
182 return false;
183 }
184 try {
185 const json document = json::parse(input);
186 if (document.value("schema_version", 0) != 1 ||
187 !document.contains("corrections") || !document["corrections"].is_array()) {
188 reason = std::string("invalid correction-table schema in ") + filename;
189 return false;
190 }
191 corrections = document["corrections"];
192 } catch (const std::exception &error) {
193 reason = std::string("cannot parse ") + filename + ": " + error.what();
194 return false;
195 }
196 return true;
197}
198
199bool validate_import_expectations(const std::string &manifest_filename,
200 int corrected_source_fields,
201 int alternative_rows,
202 int incompatible_alternative_rows,
203 std::string &reason)
204{
205 if (manifest_filename.empty() ||
206 !std::filesystem::is_regular_file(manifest_filename))
207 return true;
208 try {
209 std::ifstream input(manifest_filename);
210 const json document = json::parse(input);
211 if (!document.contains("import_expectations") ||
212 !document["import_expectations"].is_object()) {
213 reason = "missing import_expectations in " + manifest_filename;
214 return false;
215 }
216 const auto &expected = document["import_expectations"];
217 const std::array<std::pair<const char *, int>, 3> observed{{
218 {"corrected_source_fields", corrected_source_fields},
219 {"alternative_moment_rows", alternative_rows},
220 {"incompatible_alternative_moment_rows", incompatible_alternative_rows},
221 }};
222 for (const auto &entry : observed) {
223 if (!expected.contains(entry.first) ||
224 !expected[entry.first].is_number_integer()) {
225 reason = std::string("missing integer import expectation '") +
226 entry.first + "' in " + manifest_filename;
227 return false;
228 }
229 const int wanted = expected[entry.first].get<int>();
230 if (wanted != entry.second) {
231 reason = std::string(entry.first) + " changed from " +
232 std::to_string(wanted) + " to " + std::to_string(entry.second);
233 return false;
234 }
235 }
236 } catch (const std::exception &error) {
237 reason = "cannot validate " + manifest_filename + ": " + error.what();
238 return false;
239 }
240 return true;
241}
242
243template <typename Moment>
244double uncertainty_toward(const Moment &moment, bool upward)
245{
246 if (!moment.has_uncertainty) return -1.;
247 if (!moment.asymmetric) return moment.uncertainty;
248 return upward ? moment.uncertainty_high : moment.uncertainty_low;
249}
250
251template <typename Moment>
252bool alternatives_are_incompatible(const Moment &first, const Moment &second)
253{
254 if ((first.value < 0. && second.value > 0.) ||
255 (first.value > 0. && second.value < 0.))
256 return true;
257 const bool second_is_higher = second.value > first.value;
258 const double first_error = uncertainty_toward(first, second_is_higher);
259 const double second_error = uncertainty_toward(second, !second_is_higher);
260 if (first_error < 0. || second_error < 0.) return false;
261 const double combined = std::hypot(first_error, second_error);
262 return combined > 0. && std::abs(first.value - second.value) > 3. * combined;
263}
264
265std::vector<spin_value> parse_spin_values(const std::string &text,
266 int mass,
267 bool repair_invalid_denominator,
268 bool &repaired)
269{
270 std::string value = normalized_spin(text);
271 value = std::regex_replace(value, std::regex(R"((\d+)-feb)"), "$1/2");
272 value = std::regex_replace(value, std::regex("or"), ",");
273 std::replace(value.begin(), value.end(), '|', ',');
274
275 const char common_parity = !value.empty() &&
276 (value.back() == '+' || value.back() == '-') ? value.back() : '\0';
277 std::vector<spin_value> result;
278 std::stringstream stream(value);
279 std::string alternative;
280 const std::regex expression(R"(^(\d+)(?:/(\d+))?([+-]?)$)");
281 while (std::getline(stream, alternative, ',')) {
282 if (alternative.empty()) continue;
283 if (common_parity && alternative.back() != '+' && alternative.back() != '-')
284 alternative += common_parity;
285 std::smatch match;
286 if (!std::regex_match(alternative, match, expression)) continue;
287 spin_value parsed;
288 parsed.numerator = std::stoi(match[1].str());
289 parsed.denominator = match[2].matched ? std::stoi(match[2].str()) : 1;
290
291 // Odd-A nuclei have half-integer spin. The Stone value 39/3 for 215Fr
292 // is therefore non-physical and can be tested as a denominator-3 to
293 // denominator-2 transcription error when the corrected value identifies
294 // one adopted level.
295 if (repair_invalid_denominator && mass % 2 != 0 &&
296 parsed.denominator == 3 && parsed.numerator % 2 != 0) {
297 parsed.denominator = 2;
298 repaired = true;
299 }
300 result.push_back(parsed);
301 }
302 return result;
303}
304
305bool same_spin_magnitude(const std::string &source,
306 const std::string &target,
307 int mass,
308 bool &repaired)
309{
310 bool source_repaired = false;
311 bool target_repaired = false;
312 const auto source_values = parse_spin_values(source, mass, true, source_repaired);
313 const auto target_values = parse_spin_values(target, mass, false, target_repaired);
314 const bool matches = std::any_of(
315 source_values.begin(), source_values.end(),
316 [&target_values](const auto &left) {
317 return std::any_of(
318 target_values.begin(), target_values.end(),
319 [&left](const auto &right) {
320 return left.numerator * right.denominator ==
321 right.numerator * left.denominator;
322 });
323 });
324 if (matches) repaired = source_repaired;
325 return matches;
326}
327
328enum class lifetime_kind { unknown, finite, stable };
329
330struct lifetime_value {
331 lifetime_kind kind = lifetime_kind::unknown;
332 double seconds = 0.;
333 char relation = '=';
334};
335
336std::string normalize_lifetime_unit(std::string unit)
337{
338 std::transform(unit.begin(), unit.end(), unit.begin(), [](unsigned char character) {
339 return static_cast<char>(std::tolower(character));
340 });
341 std::size_t position = 0;
342 while ((position = unit.find("\xC2\xB5", position)) != std::string::npos)
343 unit.replace(position, 2, "u");
344 position = 0;
345 while ((position = unit.find("\xCE\xBC", position)) != std::string::npos)
346 unit.replace(position, 2, "u");
347 return unit;
348}
349
350double time_unit_seconds(const std::string &unit)
351{
352 if (unit == "ys") return 1.e-24;
353 if (unit == "zs") return 1.e-21;
354 if (unit == "as") return 1.e-18;
355 if (unit == "fs") return 1.e-15;
356 if (unit == "ps") return 1.e-12;
357 if (unit == "ns") return 1.e-9;
358 if (unit == "us") return 1.e-6;
359 if (unit == "ms") return 1.e-3;
360 if (unit == "s") return 1.;
361 if (unit == "m" || unit == "min") return 60.;
362 if (unit == "h") return 3600.;
363 if (unit == "d") return 86400.;
364 if (unit == "y") return 31557600.;
365 return 0.;
366}
367
368lifetime_value parse_source_lifetime(const std::string &text)
369{
370 lifetime_value result;
371 std::string value = trim(text);
372 std::transform(value.begin(), value.end(), value.begin(), [](unsigned char character) {
373 return static_cast<char>(std::tolower(character));
374 });
375 if (value.empty() || value == "-") return result;
376 if (value == "stable") {
377 result.kind = lifetime_kind::stable;
378 return result;
379 }
380
381 const std::regex expression(
382 R"(^\s*([<>~]?)([0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[xX]10\^?([+-]?\d+))?(?:\‍([^)]*\))?\s*([A-Za-z\xC2\xB5\xCE\xBC]+))");
383 std::smatch match;
384 if (!std::regex_search(value, match, expression)) return result;
385 try {
386 double numeric = std::stod(match[2].str());
387 if (match[3].matched) numeric *= std::pow(10., std::stoi(match[3].str()));
388 const double multiplier = time_unit_seconds(normalize_lifetime_unit(match[4].str()));
389 if (multiplier <= 0.) return result;
390 result.kind = lifetime_kind::finite;
391 result.seconds = numeric * multiplier;
392 result.relation = match[1].str().empty() || match[1].str() == "~"
393 ? '=' : match[1].str()[0];
394 } catch (...) {
395 return lifetime_value{};
396 }
397 return result;
398}
399
400lifetime_value candidate_lifetime(double value,
401 const std::string &unit_text,
402 bool has_value,
403 bool stable)
404{
405 lifetime_value result;
406 if (stable) {
407 result.kind = lifetime_kind::stable;
408 return result;
409 }
410 if (!has_value) return result;
411 const std::string unit = normalize_lifetime_unit(trim(unit_text));
412 const double multiplier = time_unit_seconds(unit);
413 if (multiplier > 0.) {
414 result.kind = lifetime_kind::finite;
415 result.seconds = value * multiplier;
416 return result;
417 }
418
419 double energy_multiplier = 0.;
420 if (unit == "ev") energy_multiplier = 1.;
421 if (unit == "kev") energy_multiplier = 1.e3;
422 if (unit == "mev") energy_multiplier = 1.e6;
423 if (energy_multiplier > 0. && value > 0.) {
424 constexpr double hbar_eV_seconds = 6.582119569e-16;
425 result.kind = lifetime_kind::finite;
426 result.seconds = hbar_eV_seconds * std::log(2.) / (value * energy_multiplier);
427 }
428 return result;
429}
430
431bool compatible_lifetimes(const std::string &source,
432 double candidate_value,
433 const std::string &candidate_unit,
434 bool candidate_has_value,
435 bool candidate_stable)
436{
437 const auto left = parse_source_lifetime(source);
438 const auto right = candidate_lifetime(
439 candidate_value, candidate_unit, candidate_has_value, candidate_stable);
440 if (left.kind == lifetime_kind::unknown || right.kind == lifetime_kind::unknown)
441 return false;
442 if (left.kind == lifetime_kind::stable || right.kind == lifetime_kind::stable)
443 return left.kind == right.kind;
444 if (left.seconds <= 0. || right.seconds <= 0.) return false;
445 if (left.relation == '>') return right.seconds >= left.seconds / 2.;
446 if (left.relation == '<') return right.seconds <= left.seconds * 2.;
447 const double ratio = std::max(left.seconds / right.seconds,
448 right.seconds / left.seconds);
449 return ratio <= 2. + 1.e-12;
450}
451
452} // namespace
453
454tkmoments_builder::tkmoments_builder(tkdatabase *_database) : fDataBase(_database) {}
455
456int tkmoments_builder::fill_database(const char *_magnetic_filename,
457 const char *_quadrupole_filename,
458 int _only_charge,
459 int _only_mass)
460{
461 const std::filesystem::path magnetic_path(_magnetic_filename);
462 const std::filesystem::path quadrupole_path(_quadrupole_filename);
463 if (magnetic_path.parent_path() == quadrupole_path.parent_path()) {
464 const std::filesystem::path corrections =
465 magnetic_path.parent_path() / "source-corrections.json";
466 if (std::filesystem::is_regular_file(corrections)) {
467 const std::string filename = corrections.string();
468 return fill_database(
469 _magnetic_filename, _quadrupole_filename, _only_charge, _only_mass,
470 filename.c_str());
471 }
472 }
473 return fill_database(
474 _magnetic_filename, _quadrupole_filename, _only_charge, _only_mass,
475 nullptr);
476}
477
478int tkmoments_builder::fill_database(const char *_magnetic_filename,
479 const char *_quadrupole_filename,
480 int _only_charge,
481 int _only_mass,
482 const char *_corrections_filename)
483{
484 fImportedGroundStates = 0;
485 fImportedLevels = 0;
486 fSkippedValues = 0;
487 fUnmatchedLevels = 0;
488 fSourceRows = 0;
489 fLevelCandidateRows = 0;
490 fPhysicallyForbidden = 0;
491 fRelaxedLevelMatches = 0;
492 fParityRelaxedMatches = 0;
493 fRepairedSpinMatches = 0;
494 fCorrectedSourceFields = 0;
495 fAlternativeMomentRows = 0;
496 fIncompatibleAlternativeMomentRows = 0;
497
498 if (!fDataBase || !fDataBase->get_sql_db() || !load_targets()) return 1;
499
500 fDataBase->exec_sql("BEGIN TRANSACTION");
501 if (!clear_existing_moments(_only_charge, _only_mass)) {
502 fDataBase->exec_sql("ROLLBACK");
503 return 1;
504 }
505 const int magnetic_status = import_file(
506 _magnetic_filename,
507 "magnetic dipole [nm]",
508 "magnetic_dipole",
509 "mun",
510 _only_charge,
511 _only_mass,
512 _corrections_filename);
513 const int quadrupole_status = import_file(
514 _quadrupole_filename,
515 "electric quadrupole [b]",
516 "electric_quadrupole",
517 "barn",
518 _only_charge,
519 _only_mass,
520 _corrections_filename);
521
522 if (magnetic_status || quadrupole_status) {
523 fDataBase->exec_sql("ROLLBACK");
524 return 1;
525 }
526
527 if (fSourceRows >= 100 && fSkippedValues * 100 > fSourceRows * 2) {
528 glog << error << "Rejected " << fSkippedValues << " of " << fSourceRows
529 << " Stone rows (>2%); refusing to publish a partially parsed source"
530 << do_endl;
531 fDataBase->exec_sql("ROLLBACK");
532 return 1;
533 }
534 if (fHasLevelTable && fLevelCandidateRows >= 100 &&
535 fUnmatchedLevels * 100 > fLevelCandidateRows * 10) {
536 glog << error << "Could not match " << fUnmatchedLevels << " of "
537 << fLevelCandidateRows
538 << " Stone level candidates (>10%); refusing the import" << do_endl;
539 fDataBase->exec_sql("ROLLBACK");
540 return 1;
541 }
542 if (!_only_charge && !_only_mass) {
543 const std::filesystem::path magnetic_path(_magnetic_filename);
544 const std::filesystem::path quadrupole_path(_quadrupole_filename);
545 if (magnetic_path.parent_path() == quadrupole_path.parent_path()) {
546 std::string expectation_error;
547 const std::string manifest =
548 (magnetic_path.parent_path() / "fallback-manifest.json").string();
549 if (!validate_import_expectations(
550 manifest, fCorrectedSourceFields, fAlternativeMomentRows,
551 fIncompatibleAlternativeMomentRows, expectation_error)) {
552 glog << error << "IAEA/Stone import contract changed: "
553 << expectation_error << "; refusing the import until reviewed"
554 << do_endl;
555 fDataBase->exec_sql("ROLLBACK");
556 return 1;
557 }
558 }
559 }
560 fDataBase->exec_sql("END TRANSACTION");
561
562 glog << info << "Imported IAEA/Stone moments: "
563 << fImportedGroundStates << " ground-state properties and "
564 << fImportedLevels << " adopted-level properties" << do_endl;
565 if (fSkippedValues > 0)
566 glog << warning << fSkippedValues
567 << " Stone moment rows have a non-scalar or invalid value and were skipped"
568 << do_endl;
569 if (fUnmatchedLevels > 0)
570 glog << warning << fUnmatchedLevels
571 << " Stone moment rows could not be matched unambiguously to an ENSDF adopted level"
572 << do_endl;
573 if (fPhysicallyForbidden > 0)
574 glog << warning << fPhysicallyForbidden
575 << " physically forbidden static-moment assignments were rejected"
576 << do_endl;
577 if (fRelaxedLevelMatches > 0)
578 glog << info << fRelaxedLevelMatches
579 << " Stone moment rows were matched by the guarded energy/lifetime fallback; "
580 << fParityRelaxedMatches << " ignored a superseded parity assignment"
581 << do_endl;
582 if (fRepairedSpinMatches > 0)
583 glog << warning << fRepairedSpinMatches
584 << (fRepairedSpinMatches == 1
585 ? " malformed Stone spin assignment was conservatively repaired for matching"
586 : " malformed Stone spin assignments were conservatively repaired for matching")
587 << do_endl;
588 if (fCorrectedSourceFields > 0)
589 glog << info << fCorrectedSourceFields
590 << " narrowly identified IAEA/Stone source defects were detected and repaired"
591 << do_endl;
592 if (fAlternativeMomentRows > 0)
593 glog << info << fAlternativeMomentRows
594 << " alternative IAEA/Stone moment rows retained their first value as uncertain; "
595 "all alternatives remain in the property provenance"
596 << do_endl;
597 if (fIncompatibleAlternativeMomentRows > 0)
598 glog << warning << fIncompatibleAlternativeMomentRows
599 << " alternative IAEA/Stone moment rows contain opposite-sign or >3-sigma solutions"
600 << do_endl;
601
602 return 0;
603}
604
605bool tkmoments_builder::clear_existing_moments(int _only_charge, int _only_mass) const
606{
607 std::string filter;
608 if (_only_charge)
609 filter += " AND e.charge=" + std::to_string(_only_charge);
610 if (_only_mass)
611 filter += " AND i.mass=" + std::to_string(_only_mass);
612
613 const std::string isotope_ids =
614 "SELECT i.isotope_id FROM isotope i "
615 "JOIN element e ON e.element_id=i.element_id WHERE 1=1" + filter;
616 const std::string clear_isotopes =
617 "UPDATE isotope SET "
618 "magnetic_dipole=NULL,magnetic_dipole_unit=NULL,magnetic_dipole_unc=NULL,"
619 "magnetic_dipole_unc_low=NULL,magnetic_dipole_unc_high=NULL,magnetic_dipole_info=NULL,"
620 "electric_quadrupole=NULL,electric_quadrupole_unit=NULL,electric_quadrupole_unc=NULL,"
621 "electric_quadrupole_unc_low=NULL,electric_quadrupole_unc_high=NULL,electric_quadrupole_info=NULL "
622 "WHERE isotope_id IN (" + isotope_ids + ")";
623 if (fDataBase->exec_sql(clear_isotopes.c_str()) != SQLITE_OK) return false;
624
625 if (!fDataBase->has_table("LEVEL")) return true;
626 const std::string clear_levels =
627 "UPDATE level SET "
628 "level_magnetic_dipole=NULL,level_magnetic_dipole_unit=NULL,level_magnetic_dipole_unc=NULL,"
629 "level_magnetic_dipole_unc_low=NULL,level_magnetic_dipole_unc_high=NULL,level_magnetic_dipole_info=NULL,"
630 "level_electric_quadrupole=NULL,level_electric_quadrupole_unit=NULL,level_electric_quadrupole_unc=NULL,"
631 "level_electric_quadrupole_unc_low=NULL,level_electric_quadrupole_unc_high=NULL,level_electric_quadrupole_info=NULL "
632 "WHERE isotope_id IN (" + isotope_ids + ")";
633 return fDataBase->exec_sql(clear_levels.c_str()) == SQLITE_OK;
634}
635
636bool tkmoments_builder::load_targets()
637{
638 fHasLevelTable = false;
639 fIsotopeIds.clear();
640 fIsotopeStable.clear();
641 fAdoptedLevels.clear();
642 fLevelSpins.clear();
643
644 sqlite3 *database = fDataBase->get_sql_db();
645 sqlite3_stmt *statement = nullptr;
646 const char *isotope_sql =
647 "SELECT e.charge, i.mass, i.isotope_id, COALESCE(i.lifetime_info,'') "
648 "FROM isotope i JOIN element e ON e.element_id=i.element_id";
649 if (sqlite3_prepare_v2(database, isotope_sql, -1, &statement, nullptr) != SQLITE_OK)
650 return false;
651 while (sqlite3_step(statement) == SQLITE_ROW) {
652 const nucleus_key nucleus{
653 sqlite3_column_int(statement, 0), sqlite3_column_int(statement, 1)};
654 fIsotopeIds[nucleus] = sqlite3_column_int(statement, 2);
655 fIsotopeStable[nucleus] = sqlite_text(statement, 3).find(";STABLE") != std::string::npos;
656 }
657 sqlite3_finalize(statement);
658
659 if (!fDataBase->has_table("LEVEL") || !fDataBase->has_table("DATASET")) return true;
660 fHasLevelTable = true;
661
662 const char *level_sql =
663 "SELECT e.charge, i.mass, l.level_id, l.level_energy, "
664 "COALESCE(l.level_energy_info,''), COALESCE(l.level_spin_parity,''), "
665 "l.level_lifetime, COALESCE(l.level_lifetime_unit,''), "
666 "COALESCE(l.level_lifetime_info,'') "
667 "FROM level l "
668 "JOIN dataset d ON d.dataset_id=l.dataset_id "
669 "JOIN isotope i ON i.isotope_id=l.isotope_id "
670 "JOIN element e ON e.element_id=i.element_id "
671 "WHERE d.dataset_source='ENSDF' "
672 "AND d.dataset_name LIKE '%ADOPTED LEVELS%'";
673 if (sqlite3_prepare_v2(database, level_sql, -1, &statement, nullptr) != SQLITE_OK)
674 return false;
675
676 while (sqlite3_step(statement) == SQLITE_ROW) {
677 level_candidate candidate;
678 candidate.id = sqlite3_column_int(statement, 2);
679 candidate.energy = sqlite3_column_double(statement, 3);
680 const std::string energy_info = sqlite_text(statement, 4);
681 const std::string marker = ";OFF=";
682 const auto offset_position = energy_info.find(marker);
683 if (offset_position != std::string::npos && offset_position + marker.size() < energy_info.size())
684 candidate.offset = std::string(1, static_cast<char>(std::toupper(
685 static_cast<unsigned char>(energy_info[offset_position + marker.size()]))));
686 candidate.spin_parity = sqlite_text(statement, 5);
687 fLevelSpins[candidate.id] = candidate.spin_parity;
688 candidate.has_lifetime = sqlite3_column_type(statement, 6) != SQLITE_NULL;
689 if (candidate.has_lifetime) candidate.lifetime = sqlite3_column_double(statement, 6);
690 candidate.lifetime_unit = sqlite_text(statement, 7);
691 candidate.stable = sqlite_text(statement, 8).find(";STABLE") != std::string::npos;
692 fAdoptedLevels[{sqlite3_column_int(statement, 0), sqlite3_column_int(statement, 1)}]
693 .push_back(candidate);
694 }
695 sqlite3_finalize(statement);
696 return true;
697}
698
699int tkmoments_builder::import_file(const char *_filename,
700 const std::string &_moment_column,
701 const std::string &_property_name,
702 const std::string &_unit,
703 int _only_charge,
704 int _only_mass,
705 const char *_corrections_filename)
706{
707 std::ifstream input(_filename);
708 if (!input.good()) {
709 glog << error << "IAEA/Stone moment file cannot be opened: " << _filename << do_endl;
710 return 1;
711 }
712
713 std::string line;
714 if (!std::getline(input, line)) return 1;
715 std::vector<std::string> header;
716 if (!detail::parse_csv_row(line, header)) return 1;
717 std::map<std::string, std::size_t> columns;
718 for (std::size_t index = 0; index < header.size(); ++index)
719 columns[trim(header[index])] = index;
720
721 const std::vector<std::string> required = {
722 "z", "n.n+n.z", "energy [keV]", "spin", _moment_column,
723 "method", "nsr", "indc"};
724 const auto missing_column = std::find_if(
725 required.begin(), required.end(),
726 [&columns](const auto &name) { return !columns.count(name); });
727 if (missing_column != required.end()) {
728 glog << error << "Invalid IAEA/Stone CSV header in " << _filename
729 << ": missing column '" << *missing_column << "'" << do_endl;
730 return 1;
731 }
732
733 json corrections;
734 std::string correction_error;
735 if (!load_source_corrections(
736 _corrections_filename, corrections, correction_error)) {
737 glog << error << "Invalid IAEA/Stone source-correction table: "
738 << correction_error << do_endl;
739 return 1;
740 }
741
742 int line_number = 1;
743 std::set<std::string> seen_source_states;
744 while (std::getline(input, line)) {
745 ++line_number;
746 ++fSourceRows;
747 std::vector<std::string> row;
748 if (!detail::parse_csv_row(line, row) || row.size() != header.size()) {
749 ++fSkippedValues;
750 glog << warning << _filename << ":" << line_number
751 << ": CSV row width does not match the header" << do_endl;
752 continue;
753 }
754
755 const int charge = std::atoi(column_value(row, columns, "z").c_str());
756 const int mass = std::atoi(column_value(row, columns, "n.n+n.z").c_str());
757 if ((_only_charge && charge != _only_charge) || (_only_mass && mass != _only_mass))
758 continue;
759
760 const nucleus_key nucleus{charge, mass};
761 const auto isotope = fIsotopeIds.find(nucleus);
762 if (isotope == fIsotopeIds.end()) continue;
763
764 const std::string raw_source_spin = column_value(row, columns, "spin");
765 const std::string raw_source_moment = column_value(row, columns, _moment_column);
766 std::string source_fix;
767 std::string source_reference;
768 if (apply_source_correction(
769 row, columns, _filename, _moment_column, corrections,
770 source_reference)) {
771 source_fix = "CSV_ALIGNMENT";
772 ++fCorrectedSourceFields;
773 }
774 if (has_reversed_parity_notation(raw_source_spin)) {
775 if (!repair_reversed_parity_notation(row, columns)) {
776 ++fSkippedValues;
777 glog << warning << _filename << ":" << line_number
778 << ": failed to repair reversed spin/parity notation '"
779 << raw_source_spin << "'" << do_endl;
780 continue;
781 }
782 if (!source_fix.empty()) source_fix += ",";
783 source_fix += "SPIN_PARITY_ORDER";
784 ++fCorrectedSourceFields;
785 }
786
787 const std::string source_state_key =
788 std::to_string(charge) + ":" + std::to_string(mass) + ":" +
789 column_value(row, columns, "energy [keV]");
790 if (!seen_source_states.insert(source_state_key).second) {
791 glog << warning << _filename << ":" << line_number
792 << ": duplicate Stone state key Z=" << charge << ", A=" << mass
793 << ", E='" << column_value(row, columns, "energy [keV]")
794 << "'; lifetime/spin matching will decide whether it is distinct"
795 << do_endl;
796 }
797
798 const std::string moment_text = column_value(row, columns, _moment_column);
799 // Some IAEA tables contain placeholder rows with state metadata but no
800 // recommended moment (currently 124I). This is an ordinary absence, not
801 // a malformed value and therefore does not warrant a warning.
802 if (moment_text.empty()) continue;
803
804 parsed_moment moment = parse_moment(moment_text);
805 if (!moment.valid) {
806 ++fSkippedValues;
807 glog << warning << _filename << ":" << line_number
808 << ": skipped non-scalar or invalid moment for Z=" << charge
809 << ", A=" << mass << ": '"
810 << moment_text << "'" << do_endl;
811 continue;
812 }
813 if (!moment.source_fix.empty()) {
814 if (!source_fix.empty()) source_fix += ",";
815 source_fix += moment.source_fix;
816 ++fCorrectedSourceFields;
817 }
818 if (!moment.alternative.empty())
819 ++fAlternativeMomentRows;
820 if (moment.alternative_incompatible) {
821 ++fIncompatibleAlternativeMomentRows;
822 glog << warning << _filename << ":" << line_number
823 << ": incompatible alternative moment solutions for Z=" << charge
824 << ", A=" << mass << ": '" << raw_source_moment << "'"
825 << do_endl;
826 }
827
828 ++fLevelCandidateRows;
829 const parsed_energy energy = parse_energy(column_value(row, columns, "energy [keV]"));
830 if (!energy.valid) {
831 ++fUnmatchedLevels;
832 glog << warning << _filename << ":" << line_number
833 << ": invalid or non-numeric level energy for Z=" << charge
834 << ", A=" << mass << ": '"
835 << column_value(row, columns, "energy [keV]") << "'" << do_endl;
836 continue;
837 }
838
839 std::string provenance = moment.info;
840 append_tag(provenance, "SRC=STONE");
841 append_tag(provenance, "NSR=" + column_value(row, columns, "nsr"));
842 append_tag(provenance, "INDC=" + column_value(row, columns, "indc"));
843 append_tag(provenance, "METHOD=" + column_value(row, columns, "method"));
844 append_tag(provenance, "RAW=" + raw_source_moment);
845 if (!source_fix.empty()) append_tag(provenance, "SRCFIX=" + source_fix);
846 if (!source_reference.empty())
847 append_tag(provenance, "SRCREF=" + source_reference);
848 if (has_reversed_parity_notation(raw_source_spin))
849 append_tag(provenance, "RAW_SPIN=" + raw_source_spin);
850
851 const level_match matched_level = fHasLevelTable
852 ? find_level(
853 nucleus, energy, column_value(row, columns, "spin"),
854 column_value(row, columns, "halflife"))
855 : level_match{};
856 const int level_id = matched_level.id;
857 if (!matched_level.method.empty()) {
858 if (provenance.find("RAW_SPIN=") == std::string::npos)
859 append_tag(provenance, "RAW_SPIN=" + raw_source_spin);
860 append_tag(provenance, "MATCH=" + matched_level.method);
861 ++fRelaxedLevelMatches;
862 if (matched_level.method == "J_ONLY") ++fParityRelaxedMatches;
863 if (matched_level.method == "SPIN_REPAIR") {
864 ++fRepairedSpinMatches;
865 glog << warning << _filename << ":" << line_number
866 << ": repaired non-physical Stone spin '"
867 << column_value(row, columns, "spin") << "' while matching ENSDF spin '"
868 << matched_level.adopted_spin << "' at Z=" << charge << ", A=" << mass
869 << do_endl;
870 }
871 }
872 level_candidate ground_state;
873 ground_state.stable = fIsotopeStable[nucleus];
874 if (energy.offset.empty() && std::abs(energy.value) < 1.e-9 &&
875 lifetime_matches(column_value(row, columns, "halflife"), ground_state)) {
876 std::string ground_spin = column_value(row, columns, "spin");
877 const auto adopted_spin = fLevelSpins.find(level_id);
878 if (adopted_spin != fLevelSpins.end()) ground_spin = adopted_spin->second;
879 if (!static_moment_allowed(_property_name, ground_spin)) {
880 ++fPhysicallyForbidden;
881 glog << warning << _filename << ":" << line_number
882 << ": physically forbidden " << _property_name
883 << " for ground-state spin '" << ground_spin
884 << "' at Z=" << charge << ", A=" << mass << do_endl;
885 } else if (update_measure("ISOTOPE", _property_name, "isotope_id",
886 isotope->second, moment, _unit, provenance)) {
887 ++fImportedGroundStates;
888 }
889 }
890
891 // An isotope-only database has no LEVEL table. Ground-state properties
892 // are still imported, but excited-state matching is deliberately skipped.
893 if (!fHasLevelTable) continue;
894
895 if (level_id < 0) {
896 ++fUnmatchedLevels;
897 glog << warning << _filename << ":" << line_number
898 << ": no unambiguous ENSDF adopted-level match for Z=" << charge
899 << ", A=" << mass << ", E='"
900 << column_value(row, columns, "energy [keV]") << "', spin='"
901 << column_value(row, columns, "spin") << "'" << do_endl;
902 continue;
903 }
904 const auto target_spin = fLevelSpins.find(level_id);
905 if (target_spin != fLevelSpins.end() &&
906 !static_moment_allowed(_property_name, target_spin->second)) {
907 ++fPhysicallyForbidden;
908 glog << warning << _filename << ":" << line_number
909 << ": physically forbidden " << _property_name
910 << " for ENSDF spin '" << target_spin->second
911 << "' at Z=" << charge << ", A=" << mass << do_endl;
912 continue;
913 }
914 if (update_measure("LEVEL", "level_" + _property_name, "level_id",
915 level_id, moment, _unit, provenance))
916 ++fImportedLevels;
917 }
918 return 0;
919}
920
921tkmoments_builder::parsed_energy tkmoments_builder::parse_energy(const std::string &_text)
922{
923 parsed_energy result;
924 std::string value = trim(_text);
925 remove_spaces(value);
926 std::transform(value.begin(), value.end(), value.begin(), [](unsigned char character) {
927 return static_cast<char>(std::tolower(character));
928 });
929
930 if (value.empty() || value == "-" || value.front() == '~' ||
931 value.find('<') != std::string::npos || value.find('>') != std::string::npos)
932 return result;
933
934 std::smatch shifted;
935 const std::regex shifted_expression(R"(^([+-]?(?:\d+(?:\.\d*)?|\.\d+))\+([a-z])$)");
936 const std::regex numeric_expression(R"(^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$)");
937 try {
938 if (std::regex_match(value, shifted, shifted_expression)) {
939 result.value = std::stod(shifted[1].str());
940 result.offset = std::string(1, static_cast<char>(std::toupper(
941 static_cast<unsigned char>(shifted[2].str()[0]))));
942 } else if (std::regex_match(value, numeric_expression)) {
943 result.value = std::stod(value);
944 } else {
945 return result;
946 }
947 } catch (...) {
948 return result;
949 }
950 result.valid = true;
951 return result;
952}
953
954tkmoments_builder::parsed_moment tkmoments_builder::parse_moment(const std::string &_text)
955{
956 parsed_moment result;
957 std::string value = trim(_text);
958 if (value.empty()) return result;
959
960 // "or" denotes discrete solutions of the experimental analysis, not
961 // independent measurements that may be averaged. TkN's scalar API keeps
962 // the first published solution, marks it uncertain, and preserves the
963 // alternative and complete raw field in the provenance.
964 const auto alternative_position = value.find(" or ");
965 if (alternative_position != std::string::npos) {
966 const std::string alternative = trim(value.substr(alternative_position + 4));
967 result = parse_moment(value.substr(0, alternative_position));
968 const parsed_moment second = parse_moment(alternative);
969 if (!result.valid || !second.valid || alternative.empty() ||
970 result.info.find("ERR=") != std::string::npos ||
971 result.info.find("VAL=()") != std::string::npos)
972 return parsed_moment{};
973 result.alternative = alternative;
974 result.alternative_incompatible =
975 alternatives_are_incompatible(result, second);
976 append_tag(result.info, "ERR=?");
977 append_tag(result.info, "ALT=" + alternative);
978 if (result.alternative_incompatible)
979 append_tag(result.info, "ALT_INCOMPATIBLE=1");
980 return result;
981 }
982
983 if (value.front() == '\'') value.erase(value.begin());
984 if (value.rfind("Ref. estimated ", 0) == 0) {
985 value.erase(0, 15);
986 append_tag(result.info, "ERR=CA");
987 } else if (value.rfind("estimated ", 0) == 0) {
988 value.erase(0, 10);
989 append_tag(result.info, "ERR=CA");
990 }
991
992 const std::regex spin_prefix(R"(^\‍(I\s*=\s*[^)]+\)\s*)");
993 const std::regex spin_suffix(R"(\s*\‍(I\s*=\s*[^)]+\)\s*$)");
994 value = std::regex_replace(value, spin_prefix, "");
995 value = std::regex_replace(value, spin_suffix, "");
996
997 if (value.size() > 1 && value.front() == '[' && value.back() == ']') {
998 value = value.substr(1, value.size() - 2);
999 append_tag(result.info, "ERR=CA");
1000 }
1001
1002 if (value.rfind("(+)", 0) == 0 || value.rfind("(-)", 0) == 0) {
1003 const char sign = value[1];
1004 value = sign + value.substr(3);
1005 append_tag(result.info, "VAL=()");
1006 }
1007
1008 if (!value.empty() && (value.front() == '<' || value.front() == '>')) {
1009 append_tag(result.info, value.front() == '<' ? "ERR=LT" : "ERR=GT");
1010 value.erase(value.begin());
1011 }
1012 remove_spaces(value);
1013
1014 // Repair only syntax errors for which a unique one-character correction
1015 // produces a standard Stone value. These patterns cover the currently
1016 // identified duplicated/misplaced parenthesis and the truncated closing
1017 // parenthesis of an asymmetric uncertainty.
1018 bool repaired_syntax = false;
1019 const auto duplicated_parenthesis = value.find("((");
1020 if (duplicated_parenthesis != std::string::npos &&
1021 value.find("((", duplicated_parenthesis + 2) == std::string::npos) {
1022 value.erase(duplicated_parenthesis, 1);
1023 repaired_syntax = true;
1024 }
1025
1026 std::smatch misplaced_uncertainty;
1027 const std::regex misplaced_uncertainty_expression(
1028 R"(^([+-]?(?:\d+(?:\.\d*)?|\.\d+))\)([0-9]+(?:\.[0-9]+)?)\)$)");
1029 if (std::regex_match(value, misplaced_uncertainty,
1030 misplaced_uncertainty_expression)) {
1031 value = misplaced_uncertainty[1].str() + "(" +
1032 misplaced_uncertainty[2].str() + ")";
1033 repaired_syntax = true;
1034 }
1035
1036 // This exact IAEA truncation is independently corroborated by Table 1 of
1037 // the same authors' KISS report (2021), which gives
1038 // mu(198Ir)=+0.13(+0.10/-0.02) mu_N and cites 2020Mu16. Do not generalize
1039 // the repair: another truncated final digit would not be uniquely known.
1040 if (value == "+0.13(+10,-2") {
1041 value += ")";
1042 repaired_syntax = true;
1043 }
1044 if (repaired_syntax) result.source_fix = "MOMENT_SYNTAX";
1045
1046 std::smatch match;
1047 const std::regex asymmetric_expression(
1048 R"(^([+-]?(?:\d+(?:\.\d*)?|\.\d+))\‍(\+([0-9]+(?:\.[0-9]+)?)[,/]?-([0-9]+(?:\.[0-9]+)?)\)$)");
1049 const std::regex symmetric_expression(
1050 R"(^([+-]?(?:\d+(?:\.\d*)?|\.\d+))(?:\‍(([0-9]+(?:\.[0-9]+)?)\))?([A-Za-z#*?]*)$)");
1051
1052 try {
1053 if (std::regex_match(value, match, asymmetric_expression)) {
1054 const std::string central = match[1].str();
1055 result.value = std::stod(central);
1056 result.uncertainty_high = uncertainty_from_digits(match[2].str(), central);
1057 result.uncertainty_low = uncertainty_from_digits(match[3].str(), central);
1058 result.asymmetric = true;
1059 result.has_uncertainty = true;
1060 } else if (std::regex_match(value, match, symmetric_expression)) {
1061 const std::string central = match[1].str();
1062 result.value = std::stod(central);
1063 if (match[2].matched) {
1064 result.uncertainty = uncertainty_from_digits(match[2].str(), central);
1065 result.has_uncertainty = true;
1066 } else if (result.info.find("ERR=LT") == std::string::npos &&
1067 result.info.find("ERR=GT") == std::string::npos) {
1068 append_tag(result.info, "ERR=?");
1069 }
1070 if (match[3].matched && !match[3].str().empty())
1071 append_tag(result.info, "NOTE=" + match[3].str());
1072 } else {
1073 return result;
1074 }
1075 } catch (...) {
1076 return result;
1077 }
1078
1079 result.valid = true;
1080 return result;
1081}
1082
1083std::string tkmoments_builder::normalize_spin(const std::string &_text)
1084{
1085 return normalized_spin(_text);
1086}
1087
1088bool tkmoments_builder::spin_matches(const std::string &_stone_spin,
1089 const std::string &_ensdf_spin)
1090{
1091 auto alternatives = [](std::string value) {
1092 value = normalize_spin(value);
1093 value = std::regex_replace(value, std::regex(R"((\d+)-feb)"), "$1/2");
1094 value = std::regex_replace(value, std::regex("or"), ",");
1095 std::replace(value.begin(), value.end(), '|', ',');
1096
1097 const char common_parity = !value.empty() &&
1098 (value.back() == '+' || value.back() == '-') ? value.back() : '\0';
1099 std::vector<std::string> result;
1100 std::stringstream stream(value);
1101 std::string alternative;
1102 while (std::getline(stream, alternative, ',')) {
1103 if (alternative.empty()) continue;
1104 if (common_parity && alternative.back() != '+' && alternative.back() != '-')
1105 alternative += common_parity;
1106 result.push_back(alternative);
1107 }
1108 return result;
1109 };
1110
1111 const auto source = alternatives(_stone_spin);
1112 const auto target = alternatives(_ensdf_spin);
1113 return std::any_of(source.begin(), source.end(), [&target](const auto &left) {
1114 return std::any_of(target.begin(), target.end(), [&left](const auto &right) {
1115 if (left == right) return true;
1116 const bool left_has_parity = left.back() == '+' || left.back() == '-';
1117 const bool right_has_parity = right.back() == '+' || right.back() == '-';
1118 const std::string left_spin = left_has_parity ? left.substr(0, left.size() - 1) : left;
1119 const std::string right_spin = right_has_parity ? right.substr(0, right.size() - 1) : right;
1120 return left_spin == right_spin && (!left_has_parity || !right_has_parity);
1121 });
1122 });
1123}
1124
1125bool tkmoments_builder::static_moment_allowed(const std::string &_property_name,
1126 const std::string &_spin)
1127{
1128 std::string value = normalize_spin(_spin);
1129 value = std::regex_replace(value, std::regex("or"), ",");
1130 std::replace(value.begin(), value.end(), '|', ',');
1131
1132 bool parsed_any = false;
1133 bool every_alternative_forbidden = true;
1134 std::stringstream alternatives(value);
1135 std::string alternative;
1136 while (std::getline(alternatives, alternative, ',')) {
1137 if (alternative.empty()) continue;
1138 if (alternative.back() == '+' || alternative.back() == '-')
1139 alternative.pop_back();
1140
1141 std::smatch match;
1142 const std::regex spin_expression(R"(^(\d+)(?:/(\d+))?$)");
1143 if (!std::regex_match(alternative, match, spin_expression)) return true;
1144 const double numerator = std::stod(match[1].str());
1145 const double denominator = match[2].matched ? std::stod(match[2].str()) : 1.;
1146 if (denominator == 0.) return true;
1147 const double spin = numerator / denominator;
1148 parsed_any = true;
1149 const bool forbidden = _property_name == "magnetic_dipole"
1150 ? std::abs(spin) < 1.e-12
1151 : spin < 1.;
1152 every_alternative_forbidden = every_alternative_forbidden && forbidden;
1153 }
1154 return !parsed_any || !every_alternative_forbidden;
1155}
1156
1157bool tkmoments_builder::lifetime_matches(const std::string &_stone_lifetime,
1158 const level_candidate &_candidate)
1159{
1160 std::string lifetime = trim(_stone_lifetime);
1161 std::transform(lifetime.begin(), lifetime.end(), lifetime.begin(),
1162 [](unsigned char character) { return static_cast<char>(std::tolower(character)); });
1163 if (lifetime.empty() || lifetime == "-") return true;
1164 // Stone may label observationally stable nuclei as "stable" while a newer
1165 // ENSDF evaluation gives an extremely long finite half-life.
1166 if (lifetime == "stable") return true;
1167 // A finite-lived Stone entry cannot denote an ENSDF level marked stable.
1168 // Numeric lifetimes are otherwise only supporting metadata: the adopted
1169 // value may have changed between evaluations.
1170 return !_candidate.stable;
1171}
1172
1173tkmoments_builder::level_match tkmoments_builder::find_level(
1174 const nucleus_key &_nucleus,
1175 const parsed_energy &_energy,
1176 const std::string &_spin,
1177 const std::string &_lifetime) const
1178{
1179 const auto levels = fAdoptedLevels.find(_nucleus);
1180 if (levels == fAdoptedLevels.end()) return {};
1181
1182 constexpr double strict_energy_tolerance_keV = 1.;
1183 std::vector<const level_candidate *> candidates;
1184 for (const auto &candidate : levels->second) {
1185 if (!_energy.offset.empty() && candidate.offset != _energy.offset) continue;
1186 if (std::abs(candidate.energy - _energy.value) <= strict_energy_tolerance_keV &&
1187 lifetime_matches(_lifetime, candidate))
1188 candidates.push_back(&candidate);
1189 }
1190
1191 if (!candidates.empty()) {
1192 std::vector<const level_candidate *> spin_candidates;
1193 if (!trim(_spin).empty()) {
1194 std::copy_if(candidates.begin(), candidates.end(), std::back_inserter(spin_candidates),
1195 [&](const level_candidate *candidate) {
1196 return spin_matches(_spin, candidate->spin_parity);
1197 });
1198 if (spin_candidates.empty()) {
1199 std::vector<const level_candidate *> unknown_spin_candidates;
1200 std::copy_if(candidates.begin(), candidates.end(),
1201 std::back_inserter(unknown_spin_candidates),
1202 [](const level_candidate *candidate) {
1203 return trim(candidate->spin_parity).empty();
1204 });
1205 candidates = unknown_spin_candidates;
1206 } else {
1207 candidates = spin_candidates;
1208 }
1209 }
1210
1211 // A Stone energy without an explicit band offset normally denotes an
1212 // absolute ENSDF energy. Prefer it when spin/lifetime leave both absolute
1213 // and offset-band candidates; otherwise the unique offset match is valid.
1214 if (_energy.offset.empty()) {
1215 std::vector<const level_candidate *> absolute_candidates;
1216 std::copy_if(candidates.begin(), candidates.end(),
1217 std::back_inserter(absolute_candidates),
1218 [](const level_candidate *candidate) { return candidate->offset.empty(); });
1219 if (!absolute_candidates.empty()) candidates = absolute_candidates;
1220 }
1221
1222 if (candidates.size() == 1)
1223 return {candidates.front()->id, "", candidates.front()->spin_parity};
1224
1225 if (!candidates.empty()) {
1226 const auto closest = std::min_element(candidates.begin(), candidates.end(),
1227 [&](const level_candidate *left, const level_candidate *right) {
1228 return std::abs(left->energy - _energy.value) <
1229 std::abs(right->energy - _energy.value);
1230 });
1231 const double closest_difference = std::abs((*closest)->energy - _energy.value);
1232 const int equally_close = static_cast<int>(std::count_if(
1233 candidates.begin(), candidates.end(), [&](const level_candidate *candidate) {
1234 return std::abs(std::abs(candidate->energy - _energy.value) -
1235 closest_difference) < 1.e-9;
1236 }));
1237 if (equally_close == 1)
1238 return {(*closest)->id, "", (*closest)->spin_parity};
1239 }
1240 }
1241
1242 // Evaluated level energies and parity assignments may change after the
1243 // moment measurement. Search a wider window only when J and a numerical
1244 // lifetime identify exactly one adopted level. A missing lifetime is not
1245 // sufficient for this fallback.
1246 constexpr double fallback_energy_tolerance_keV = 10.;
1247 std::vector<const level_candidate *> fallback_candidates;
1248 bool repaired_spin = false;
1249 for (const auto &candidate : levels->second) {
1250 if (!_energy.offset.empty() && candidate.offset != _energy.offset) continue;
1251 if (std::abs(candidate.energy - _energy.value) > fallback_energy_tolerance_keV)
1252 continue;
1253 bool candidate_used_repair = false;
1254 if (!same_spin_magnitude(
1255 _spin, candidate.spin_parity, _nucleus.second, candidate_used_repair))
1256 continue;
1257 if (!compatible_lifetimes(
1258 _lifetime, candidate.lifetime, candidate.lifetime_unit,
1259 candidate.has_lifetime, candidate.stable))
1260 continue;
1261 fallback_candidates.push_back(&candidate);
1262 repaired_spin = repaired_spin || candidate_used_repair;
1263 }
1264
1265 if (_energy.offset.empty()) {
1266 std::vector<const level_candidate *> absolute_candidates;
1267 std::copy_if(fallback_candidates.begin(), fallback_candidates.end(),
1268 std::back_inserter(absolute_candidates),
1269 [](const level_candidate *candidate) { return candidate->offset.empty(); });
1270 if (!absolute_candidates.empty()) fallback_candidates = absolute_candidates;
1271 }
1272 if (fallback_candidates.size() != 1) return {};
1273
1274 const auto *candidate = fallback_candidates.front();
1275 std::string method = "ENERGY_LIFETIME";
1276 if (repaired_spin)
1277 method = "SPIN_REPAIR";
1278 else if (!spin_matches(_spin, candidate->spin_parity))
1279 method = "J_ONLY";
1280 return {candidate->id, method, candidate->spin_parity};
1281}
1282
1283bool tkmoments_builder::update_measure(const std::string &_table,
1284 const std::string &_prefix,
1285 const std::string &_id_column,
1286 int _id,
1287 const parsed_moment &_moment,
1288 const std::string &_unit,
1289 const std::string &_provenance) const
1290{
1291 const std::string sql =
1292 "UPDATE " + _table + " SET " +
1293 _prefix + "=?1," +
1294 _prefix + "_unit=?2," +
1295 _prefix + "_unc=?3," +
1296 _prefix + "_unc_low=?4," +
1297 _prefix + "_unc_high=?5," +
1298 _prefix + "_info=?6 WHERE " + _id_column + "=?7";
1299
1300 sqlite3_stmt *statement = nullptr;
1301 if (sqlite3_prepare_v2(fDataBase->get_sql_db(), sql.c_str(), -1, &statement, nullptr) != SQLITE_OK)
1302 return false;
1303
1304 sqlite3_bind_double(statement, 1, _moment.value);
1305 sqlite3_bind_text(statement, 2, _unit.c_str(), -1, SQLITE_TRANSIENT);
1306 if (_moment.has_uncertainty && !_moment.asymmetric)
1307 sqlite3_bind_double(statement, 3, _moment.uncertainty);
1308 else
1309 sqlite3_bind_null(statement, 3);
1310 if (_moment.has_uncertainty && _moment.asymmetric) {
1311 sqlite3_bind_double(statement, 4, _moment.uncertainty_low);
1312 sqlite3_bind_double(statement, 5, _moment.uncertainty_high);
1313 } else {
1314 sqlite3_bind_null(statement, 4);
1315 sqlite3_bind_null(statement, 5);
1316 }
1317 sqlite3_bind_text(statement, 6, _provenance.c_str(), -1, SQLITE_TRANSIENT);
1318 sqlite3_bind_int(statement, 7, _id);
1319
1320 const bool success = sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(fDataBase->get_sql_db()) == 1;
1321 sqlite3_finalize(statement);
1322 return success;
1323}
Interface to the sqlite database.
Definition tkdatabase.h:33
int exec_sql(const char *_cmd)
returns the first value for selection
bool has_table(const tkstring &_table_name)
sqlite3 * get_sql_db()
Definition tkdatabase.h:53
tkmoments_builder(tkdatabase *_database)
int fill_database(const char *_magnetic_filename, const char *_quadrupole_filename, int _only_charge=0, int _only_mass=0)
bool parse_csv_row(const std::string &line, std::vector< std::string > &fields)
Definition tkcsv.h:14
Definition tklog.cpp:16
std::map< tkstring, tkdb_column > row
Definition tkdb_table.h:28
tklog & info(tklog &log)
Definition tklog.h:313
tklog & error(tklog &log)
Definition tklog.h:344
tklog & do_endl(tklog &log)
Definition tklog.h:212
tklog & warning(tklog &log)
Definition tklog.h:331