9#include "tkmoments_builder.h"
28using json = nlohmann::json;
32std::string trim(
const std::string &value)
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);
40void remove_spaces(std::string &value)
43 std::remove_if(value.begin(), value.end(), [](
unsigned char character) {
44 return std::isspace(character) != 0;
49void append_tag(std::string &
info,
const std::string &tag)
51 if (tag.empty())
return;
55double uncertainty_from_digits(
const std::string &digits,
const std::string ¢ral)
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
61 :
static_cast<int>(central.size() - decimal - 1);
62 return std::stod(digits) * std::pow(10., -decimal_places);
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)
69 const auto position = columns.find(name);
70 if (position == columns.end() || position->second >=
row.size())
return "";
71 return trim(
row[position->second]);
74std::string sqlite_text(sqlite3_stmt *statement,
int column)
76 const auto *value = sqlite3_column_text(statement, column);
77 return value ?
reinterpret_cast<const char *
>(value) :
"";
85std::string normalized_spin(std::string value)
89 std::remove_if(value.begin(), value.end(), [](
unsigned char character) {
90 return std::isspace(character) || character ==
'(' || character ==
')' ||
91 character ==
'[' || character ==
']';
94 std::transform(value.begin(), value.end(), value.begin(), [](
unsigned char character) {
95 return static_cast<char>(std::tolower(character));
101bool has_reversed_parity_notation(
const std::string &text)
103 return std::regex_match(trim(text), std::regex(R
"(^-\d+(?:/\d+)?$)"));
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)
111 const auto position = columns.find(name);
112 if (position == columns.end() || position->second >=
row.size())
return false;
113 row[position->second] = value;
117bool repair_reversed_parity_notation(
118 std::vector<std::string> &
row,
119 const std::map<std::string, std::size_t> &columns)
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+)?)$)")))
126 return set_column_value(
row, columns,
"spin", reversed_parity[1].str() +
"-");
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)
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())
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>();
154 if (!matches)
continue;
156 std::vector<std::string> repaired =
row;
157 auto replacement_fields = correction[
"replace"].items();
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>());
166 row = std::move(repaired);
167 source_reference = correction.value(
"source_reference", std::string());
173bool load_source_corrections(
const char *filename,
177 corrections = json::array();
178 if (!filename || !filename[0])
return true;
179 std::ifstream input(filename);
181 reason = std::string(
"cannot open ") + filename;
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;
191 corrections = document[
"corrections"];
192 }
catch (
const std::exception &
error) {
193 reason = std::string(
"cannot parse ") + filename +
": " +
error.what();
199bool validate_import_expectations(
const std::string &manifest_filename,
200 int corrected_source_fields,
201 int alternative_rows,
202 int incompatible_alternative_rows,
205 if (manifest_filename.empty() ||
206 !std::filesystem::is_regular_file(manifest_filename))
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;
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},
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;
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);
236 }
catch (
const std::exception &
error) {
237 reason =
"cannot validate " + manifest_filename +
": " +
error.what();
243template <
typename Moment>
244double uncertainty_toward(
const Moment &moment,
bool upward)
246 if (!moment.has_uncertainty)
return -1.;
247 if (!moment.asymmetric)
return moment.uncertainty;
248 return upward ? moment.uncertainty_high : moment.uncertainty_low;
251template <
typename Moment>
252bool alternatives_are_incompatible(
const Moment &first,
const Moment &second)
254 if ((first.value < 0. && second.value > 0.) ||
255 (first.value > 0. && second.value < 0.))
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;
265std::vector<spin_value> parse_spin_values(
const std::string &text,
267 bool repair_invalid_denominator,
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(),
'|',
',');
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;
286 if (!std::regex_match(alternative, match, expression))
continue;
288 parsed.numerator = std::stoi(match[1].str());
289 parsed.denominator = match[2].matched ? std::stoi(match[2].str()) : 1;
295 if (repair_invalid_denominator && mass % 2 != 0 &&
296 parsed.denominator == 3 && parsed.numerator % 2 != 0) {
297 parsed.denominator = 2;
300 result.push_back(parsed);
305bool same_spin_magnitude(
const std::string &source,
306 const std::string &target,
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) {
318 target_values.begin(), target_values.end(),
319 [&left](const auto &right) {
320 return left.numerator * right.denominator ==
321 right.numerator * left.denominator;
324 if (matches) repaired = source_repaired;
328enum class lifetime_kind { unknown, finite, stable };
330struct lifetime_value {
331 lifetime_kind kind = lifetime_kind::unknown;
336std::string normalize_lifetime_unit(std::string unit)
338 std::transform(unit.begin(), unit.end(), unit.begin(), [](
unsigned char character) {
339 return static_cast<char>(std::tolower(character));
341 std::size_t position = 0;
342 while ((position = unit.find(
"\xC2\xB5", position)) != std::string::npos)
343 unit.replace(position, 2,
"u");
345 while ((position = unit.find(
"\xCE\xBC", position)) != std::string::npos)
346 unit.replace(position, 2,
"u");
350double time_unit_seconds(
const std::string &unit)
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.;
368lifetime_value parse_source_lifetime(
const std::string &text)
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));
375 if (value.empty() || value ==
"-")
return result;
376 if (value ==
"stable") {
377 result.kind = lifetime_kind::stable;
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]+))");
384 if (!std::regex_search(value, match, expression))
return result;
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];
395 return lifetime_value{};
400lifetime_value candidate_lifetime(
double value,
401 const std::string &unit_text,
405 lifetime_value result;
407 result.kind = lifetime_kind::stable;
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;
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);
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)
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)
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;
457 const char *_quadrupole_filename,
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();
469 _magnetic_filename, _quadrupole_filename, _only_charge, _only_mass,
474 _magnetic_filename, _quadrupole_filename, _only_charge, _only_mass,
479 const char *_quadrupole_filename,
482 const char *_corrections_filename)
484 fImportedGroundStates = 0;
487 fUnmatchedLevels = 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;
498 if (!fDataBase || !fDataBase->get_sql_db() || !load_targets())
return 1;
500 fDataBase->exec_sql(
"BEGIN TRANSACTION");
501 if (!clear_existing_moments(_only_charge, _only_mass)) {
502 fDataBase->exec_sql(
"ROLLBACK");
505 const int magnetic_status = import_file(
507 "magnetic dipole [nm]",
512 _corrections_filename);
513 const int quadrupole_status = import_file(
514 _quadrupole_filename,
515 "electric quadrupole [b]",
516 "electric_quadrupole",
520 _corrections_filename);
522 if (magnetic_status || quadrupole_status) {
523 fDataBase->exec_sql(
"ROLLBACK");
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"
531 fDataBase->exec_sql(
"ROLLBACK");
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");
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"
555 fDataBase->exec_sql(
"ROLLBACK");
560 fDataBase->exec_sql(
"END TRANSACTION");
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"
569 if (fUnmatchedLevels > 0)
570 glog <<
warning << fUnmatchedLevels
571 <<
" Stone moment rows could not be matched unambiguously to an ENSDF adopted level"
573 if (fPhysicallyForbidden > 0)
574 glog <<
warning << fPhysicallyForbidden
575 <<
" physically forbidden static-moment assignments were rejected"
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"
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")
588 if (fCorrectedSourceFields > 0)
589 glog <<
info << fCorrectedSourceFields
590 <<
" narrowly identified IAEA/Stone source defects were detected and repaired"
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"
597 if (fIncompatibleAlternativeMomentRows > 0)
598 glog <<
warning << fIncompatibleAlternativeMomentRows
599 <<
" alternative IAEA/Stone moment rows contain opposite-sign or >3-sigma solutions"
605bool tkmoments_builder::clear_existing_moments(
int _only_charge,
int _only_mass)
const
609 filter +=
" AND e.charge=" + std::to_string(_only_charge);
611 filter +=
" AND i.mass=" + std::to_string(_only_mass);
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;
625 if (!fDataBase->
has_table(
"LEVEL"))
return true;
626 const std::string clear_levels =
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;
636bool tkmoments_builder::load_targets()
638 fHasLevelTable =
false;
640 fIsotopeStable.clear();
641 fAdoptedLevels.clear();
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)
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;
657 sqlite3_finalize(statement);
659 if (!fDataBase->has_table(
"LEVEL") || !fDataBase->has_table(
"DATASET"))
return true;
660 fHasLevelTable =
true;
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,'') "
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)
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);
695 sqlite3_finalize(statement);
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,
705 const char *_corrections_filename)
707 std::ifstream input(_filename);
709 glog <<
error <<
"IAEA/Stone moment file cannot be opened: " << _filename <<
do_endl;
714 if (!std::getline(input, line))
return 1;
715 std::vector<std::string> header;
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;
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;
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;
743 std::set<std::string> seen_source_states;
744 while (std::getline(input, line)) {
747 std::vector<std::string>
row;
750 glog <<
warning << _filename <<
":" << line_number
751 <<
": CSV row width does not match the header" <<
do_endl;
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))
760 const nucleus_key nucleus{charge, mass};
761 const auto isotope = fIsotopeIds.find(nucleus);
762 if (isotope == fIsotopeIds.end())
continue;
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,
771 source_fix =
"CSV_ALIGNMENT";
772 ++fCorrectedSourceFields;
774 if (has_reversed_parity_notation(raw_source_spin)) {
775 if (!repair_reversed_parity_notation(
row, columns)) {
777 glog <<
warning << _filename <<
":" << line_number
778 <<
": failed to repair reversed spin/parity notation '"
779 << raw_source_spin <<
"'" <<
do_endl;
782 if (!source_fix.empty()) source_fix +=
",";
783 source_fix +=
"SPIN_PARITY_ORDER";
784 ++fCorrectedSourceFields;
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"
798 const std::string moment_text = column_value(
row, columns, _moment_column);
802 if (moment_text.empty())
continue;
804 parsed_moment moment = parse_moment(moment_text);
807 glog <<
warning << _filename <<
":" << line_number
808 <<
": skipped non-scalar or invalid moment for Z=" << charge
809 <<
", A=" << mass <<
": '"
810 << moment_text <<
"'" <<
do_endl;
813 if (!moment.source_fix.empty()) {
814 if (!source_fix.empty()) source_fix +=
",";
815 source_fix += moment.source_fix;
816 ++fCorrectedSourceFields;
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 <<
"'"
828 ++fLevelCandidateRows;
829 const parsed_energy energy = parse_energy(column_value(
row, columns,
"energy [keV]"));
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;
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);
851 const level_match matched_level = fHasLevelTable
853 nucleus, energy, column_value(
row, columns,
"spin"),
854 column_value(
row, columns,
"halflife"))
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
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;
893 if (!fHasLevelTable)
continue;
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;
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;
914 if (update_measure(
"LEVEL",
"level_" + _property_name,
"level_id",
915 level_id, moment, _unit, provenance))
921tkmoments_builder::parsed_energy tkmoments_builder::parse_energy(
const std::string &_text)
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));
930 if (value.empty() || value ==
"-" || value.front() ==
'~' ||
931 value.find(
'<') != std::string::npos || value.find(
'>') != std::string::npos)
935 const std::regex shifted_expression(R
"(^([+-]?(?:\d+(?:\.\d*)?|\.\d+))\+([a-z])$)");
936 const std::regex numeric_expression(R
"(^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$)");
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);
954tkmoments_builder::parsed_moment tkmoments_builder::parse_moment(
const std::string &_text)
956 parsed_moment result;
957 std::string value = trim(_text);
958 if (value.empty())
return result;
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");
983 if (value.front() ==
'\'') value.erase(value.begin());
984 if (value.rfind(
"Ref. estimated ", 0) == 0) {
986 append_tag(result.info,
"ERR=CA");
987 }
else if (value.rfind(
"estimated ", 0) == 0) {
989 append_tag(result.info,
"ERR=CA");
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,
"");
997 if (value.size() > 1 && value.front() ==
'[' && value.back() ==
']') {
998 value = value.substr(1, value.size() - 2);
999 append_tag(result.info,
"ERR=CA");
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=()");
1008 if (!value.empty() && (value.front() ==
'<' || value.front() ==
'>')) {
1009 append_tag(result.info, value.front() ==
'<' ?
"ERR=LT" :
"ERR=GT");
1010 value.erase(value.begin());
1012 remove_spaces(value);
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;
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;
1040 if (value ==
"+0.13(+10,-2") {
1042 repaired_syntax =
true;
1044 if (repaired_syntax) result.source_fix =
"MOMENT_SYNTAX";
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#*?]*)$)");
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=?");
1070 if (match[3].matched && !match[3].str().empty())
1071 append_tag(result.info,
"NOTE=" + match[3].str());
1079 result.valid =
true;
1083std::string tkmoments_builder::normalize_spin(
const std::string &_text)
1085 return normalized_spin(_text);
1088bool tkmoments_builder::spin_matches(
const std::string &_stone_spin,
1089 const std::string &_ensdf_spin)
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(),
'|',
',');
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);
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);
1125bool tkmoments_builder::static_moment_allowed(
const std::string &_property_name,
1126 const std::string &_spin)
1128 std::string value = normalize_spin(_spin);
1129 value = std::regex_replace(value, std::regex(
"or"),
",");
1130 std::replace(value.begin(), value.end(),
'|',
',');
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();
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;
1149 const bool forbidden = _property_name ==
"magnetic_dipole"
1150 ? std::abs(spin) < 1.e-12
1152 every_alternative_forbidden = every_alternative_forbidden && forbidden;
1154 return !parsed_any || !every_alternative_forbidden;
1157bool tkmoments_builder::lifetime_matches(
const std::string &_stone_lifetime,
1158 const level_candidate &_candidate)
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;
1166 if (lifetime ==
"stable")
return true;
1170 return !_candidate.stable;
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
1179 const auto levels = fAdoptedLevels.find(_nucleus);
1180 if (levels == fAdoptedLevels.end())
return {};
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);
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);
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();
1205 candidates = unknown_spin_candidates;
1207 candidates = spin_candidates;
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;
1222 if (candidates.size() == 1)
1223 return {candidates.front()->id,
"", candidates.front()->spin_parity};
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);
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;
1237 if (equally_close == 1)
1238 return {(*closest)->id,
"", (*closest)->spin_parity};
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)
1253 bool candidate_used_repair =
false;
1254 if (!same_spin_magnitude(
1255 _spin, candidate.spin_parity, _nucleus.second, candidate_used_repair))
1257 if (!compatible_lifetimes(
1258 _lifetime, candidate.lifetime, candidate.lifetime_unit,
1259 candidate.has_lifetime, candidate.stable))
1261 fallback_candidates.push_back(&candidate);
1262 repaired_spin = repaired_spin || candidate_used_repair;
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;
1272 if (fallback_candidates.size() != 1)
return {};
1274 const auto *candidate = fallback_candidates.front();
1275 std::string method =
"ENERGY_LIFETIME";
1277 method =
"SPIN_REPAIR";
1278 else if (!spin_matches(_spin, candidate->spin_parity))
1280 return {candidate->id, method, candidate->spin_parity};
1283bool tkmoments_builder::update_measure(
const std::string &_table,
1284 const std::string &_prefix,
1285 const std::string &_id_column,
1287 const parsed_moment &_moment,
1288 const std::string &_unit,
1289 const std::string &_provenance)
const
1291 const std::string sql =
1292 "UPDATE " + _table +
" SET " +
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";
1300 sqlite3_stmt *statement =
nullptr;
1301 if (sqlite3_prepare_v2(fDataBase->get_sql_db(), sql.c_str(), -1, &statement,
nullptr) != SQLITE_OK)
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);
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);
1314 sqlite3_bind_null(statement, 4);
1315 sqlite3_bind_null(statement, 5);
1317 sqlite3_bind_text(statement, 6, _provenance.c_str(), -1, SQLITE_TRANSIENT);
1318 sqlite3_bind_int(statement, 7, _id);
1320 const bool success = sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(fDataBase->get_sql_db()) == 1;
1321 sqlite3_finalize(statement);
Interface to the sqlite database.
int exec_sql(const char *_cmd)
returns the first value for selection
bool has_table(const tkstring &_table_name)
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)
std::map< tkstring, tkdb_column > row
tklog & error(tklog &log)
tklog & do_endl(tklog &log)
tklog & warning(tklog &log)