221 static bthg inst{auto_probe_tag{}};
254 if (request.
ages && request.
ages->min > request.
ages->max)
256 throw std::invalid_argument(
"age_range: min must be <= max");
260 throw std::invalid_argument(
"birth_request: year and ages are mutually exclusive");
263 : random_entry_(call_seed, request.
weighted);
264 return generate_(e, call_seed, request);
279 const core::batch_options& options)
300 const core::batch_options& options,
301 std::uint64_t batch_seed)
const
303 return core::run_batch(options, batch_seed, [&](std::uint64_t item_seed)
304 {
return get_birth(request, item_seed); });
320 template <
typename Sink>
322 std::uint64_t batch_seed, Sink&& sink)
const
324 core::for_each_batch(options, batch_seed,
325 [&](std::uint64_t item_seed) {
return get_birth(request, item_seed); },
326 std::forward<Sink>(sink));
335 template <
typename Sink>
339 for_each_birth(request, options, draw_seed_(), std::forward<Sink>(sink));
348 [[nodiscard]]
bool has_data()
const {
return !_entries.empty(); }
357 [[nodiscard]] std::vector<std::string>
countries()
const
359 std::vector<std::string> out;
360 out.reserve(_entries.size());
361 for (
const auto& e : _entries) out.push_back(e.cca2);
375 [[nodiscard]]
const std::vector<double>&
age_weights(std::string_view cca2, sex bio_sex)
const
377 const auto& e = lookup_entry_(cca2);
378 return bio_sex == sex::male ? e.male_weights : e.female_weights;
390 const auto& e = lookup_entry_(cca2);
391 return bio_sex == sex::male ? e.le_male : e.le_female;
400 [[nodiscard]]
static std::string_view
cohort_of(
int year) {
return cohort_label_(year); }
412 void load(
const std::filesystem::path& dir)
415 if (!std::filesystem::is_directory(dir, ec) ||
420 apply_(read_directory_(dir), core::layer_mode::replace,
true);
434 [[nodiscard]]
bool load(dataset tier)
436 const std::string tier_dir{core::tier_name(tier)};
437 const auto marker = tier_dir +
"/" + std::string{file_name_(
table::countries)};
438 const auto root = core::find_resources(library_name_, marker);
440 for (
const auto t : all_tables_)
442 docs.at(index_(t)) = core::load_resource(tier_dir +
"/" + std::string{file_name_(t)},
443 embedded_files_(), root);
446 apply_(std::move(docs), core::layer_mode::replace,
true);
462 void extend(
const std::filesystem::path& path, core::layer_mode mode = core::layer_mode::override)
465 if (std::filesystem::is_directory(path, ec))
467 apply_(read_directory_(path), mode,
false);
470 for (
const auto t : all_tables_)
472 if (path.filename() == file_name_(t))
474 extend(t, core::tsv_document::load(path), mode);
478 throw std::invalid_argument(
"extend: expected countries.tsv, age_pyramid.tsv or "
479 "monthly_births.tsv, got " + path.filename().string());
488 const core::layer_info&
extend(
table which, core::tsv_document doc,
489 core::layer_mode mode = core::layer_mode::override)
492 docs.at(index_(which)).emplace(std::move(doc));
493 apply_(std::move(docs), mode,
false);
494 return _tables.at(index_(which)).layers().back();
505 core::layer_mode mode = core::layer_mode::override)
507 return extend(which, core::tsv_document::parse(text, std::move(source)), mode);
513 [[nodiscard]]
const std::vector<core::layer_info>&
layers(
table which)
const
515 return _tables.at(index_(which)).layers();
525 for (
const auto& t : _tables)
527 any = any || !t.layers().empty();
528 h = core::hash_combine(h, t.data_hash());
534 static constexpr std::string_view library_name_ =
"birth-generator";
535 static constexpr std::size_t max_age_ = 100;
536 static constexpr std::size_t table_count_ = 3;
537 static constexpr std::array<table, table_count_> all_tables_{
539 static constexpr std::array<std::string_view, 12> month_columns_{
540 "jan",
"feb",
"mar",
"apr",
"may",
"jun",
"jul",
"aug",
"sep",
"oct",
"nov",
"dec"};
542 using documents = std::array<std::optional<core::tsv_document>, table_count_>;
543 using tables = std::array<core::layered_table, table_count_>;
545 static constexpr std::size_t index_(
table t) {
return static_cast<std::size_t
>(t); }
547 static constexpr std::string_view file_name_(
table t)
558 static const std::vector<std::string>& pyramid_columns_()
560 static const std::vector<std::string> columns = [] {
561 std::vector<std::string> c;
562 for (
const char s : {
'm',
'f'})
564 for (std::size_t a = 0; a <= max_age_; ++a) c.push_back(std::string(1, s) + std::to_string(a));
571 static core::table_schema schema_(
table t)
576 return {.key = {
"cca2"},
577 .required = {
"name",
"csection_rate"},
578 .optional = {
"cca3",
"region",
"subregion",
"latitude",
"independent",
579 "le_male",
"le_female"}};
580 case table::age_pyramid:
return {.key = {
"cca2"}, .required = pyramid_columns_(), .optional = {}};
582 return {.key = {
"cca2"},
583 .required = {month_columns_.begin(), month_columns_.end()},
589 static tables make_tables_()
602 std::optional<double> le_male;
603 std::optional<double> le_female;
604 double csection_rate{0.0};
607 static country_row parse_country_(
const core::tsv_row& row)
610 c.cca2 = row.get<std::string>(
"cca2");
611 c.name = row.get<std::string>(
"name");
612 c.csection_rate = row.get<
double>(
"csection_rate");
613 if (c.csection_rate < 0.0 || c.csection_rate > 1.0)
615 throw row.error(
"csection_rate",
"must be between 0 and 1");
617 for (
const auto& [column, value] : {std::pair{
"le_male", &c.le_male}, std::pair{
"le_female", &c.le_female}})
619 if (!row.has(column))
continue;
620 const auto v = row.get<
double>(column);
621 if (!(v > 0.0))
throw row.error(column,
"must be positive");
630 std::vector<double> male;
631 std::vector<double> female;
634 static pyramid_row parse_pyramid_(
const core::tsv_row& row)
637 const auto& columns = pyramid_columns_();
638 for (std::size_t i = 0; i < columns.size(); ++i)
640 const auto v = row.get<
double>(columns[i]);
641 if (v < 0.0)
throw row.error(columns[i],
"must not be negative");
642 (i <= max_age_ ? p.male : p.female).push_back(v);
644 for (
const auto* weights : {&p.male, &p.female})
647 for (
const double w : *weights) total += w;
650 throw row.error(weights == &p.male ?
"m0" :
"f0",
"every age of this sex is 0");
656 static std::vector<double> parse_monthly_(
const core::tsv_row& row)
658 std::vector<double> w;
660 for (
const auto column : month_columns_)
662 const auto v = row.get<
double>(column);
663 if (v < 0.0)
throw row.error(column,
"must not be negative");
667 if (!(total > 0.0))
throw row.error(
"jan",
"every month is 0");
671 static void validate_(
table t,
const core::tsv_row& row)
688 double le_female{0.0};
689 double csection_rate{0.0};
690 double total_male{0.0};
691 double total_female{0.0};
692 std::vector<double> male_weights;
693 std::vector<double> female_weights;
694 core::discrete male_age{1.0};
695 core::discrete female_age{1.0};
696 core::discrete month{1.0};
699 tables _tables{make_tables_()};
700 std::vector<entry> _entries;
701 std::map<std::string, std::size_t, std::less<>> _index;
702 core::discrete _country_dist{1.0};
703 core::rng _engine{core::rng::from_entropy()};
705 struct auto_probe_tag {};
707 explicit bthg(auto_probe_tag )
709 if (!
load(dataset::lite)) (void)
load(dataset::full);
712 static std::span<const core::embedded_file> embedded_files_()
714#ifdef DASMIG_BIRTH_EMBED
715 return embedded::birthgen::files;
721 static documents read_directory_(
const std::filesystem::path& dir)
724 for (
const auto t : all_tables_)
726 const auto file = dir / file_name_(t);
728 if (std::filesystem::is_regular_file(file, ec))
730 docs.at(index_(t)) = core::tsv_document::load(file);
738 void apply_(documents docs, core::layer_mode mode,
bool reset)
740 tables next = _tables;
741 for (
const auto t : all_tables_)
743 auto& doc = docs.at(index_(t));
744 auto& tbl = next.at(index_(t));
747 if (reset) tbl.clear();
750 (void)tbl.extend(std::move(*doc), mode, [t](
const core::tsv_row& row) { validate_(t, row); });
753 _tables = std::move(next);
758 void rebuild_(
const tables& t)
760 std::map<std::string, const core::tsv_row*, std::less<>> pyramids;
761 std::map<std::string, const core::tsv_row*, std::less<>> months;
762 for (
const auto& row : t.at(index_(
table::
age_pyramid)).rows()) pyramids.emplace(row[
"cca2"], &row);
763 for (
const auto& row : t.at(index_(
table::
monthly_births)).rows()) months.emplace(row[
"cca2"], &row);
765 std::vector<entry> entries;
766 std::map<std::string, std::size_t, std::less<>> index;
767 std::vector<double> population;
770 auto c = parse_country_(row);
771 const auto p = pyramids.find(c.cca2);
772 if (p == pyramids.end() || !c.le_male || !c.le_female)
continue;
773 auto pyramid = parse_pyramid_(*p->second);
775 e.cca2 = std::move(c.cca2);
776 e.name = std::move(c.name);
777 e.le_male = *c.le_male;
778 e.le_female = *c.le_female;
779 e.csection_rate = c.csection_rate;
780 for (
const double w : pyramid.male) e.total_male += w;
781 for (
const double w : pyramid.female) e.total_female += w;
782 e.male_age = core::discrete{pyramid.male};
783 e.female_age = core::discrete{pyramid.female};
784 e.male_weights = std::move(pyramid.male);
785 e.female_weights = std::move(pyramid.female);
786 const auto m = months.find(e.cca2);
787 e.month = m == months.end() ? core::discrete{std::vector<double>(12, 1.0)}
788 : core::discrete{parse_monthly_(*m->second)};
789 population.push_back(e.total_male + e.total_female);
790 index.emplace(e.cca2, entries.size());
791 entries.push_back(std::move(e));
793 _country_dist = population.empty() ? core::discrete{1.0} : core::discrete{population};
794 _entries = std::move(entries);
795 _index = std::move(index);
800 std::uint64_t draw_seed_() {
return _engine(); }
802 static void validate_reference_(std::chrono::year_month_day date)
806 throw std::invalid_argument(
"reference_date: not a valid date");
810 void require_data_()
const
812 if (_entries.empty())
814 throw std::runtime_error(
"No birth data loaded. Call load() first.");
818 [[nodiscard]]
const entry& lookup_entry_(std::string_view cca2)
const
821 const auto it = _index.find(cca2);
822 if (it == _index.end())
824 throw core::unknown_country_error(cca2);
826 return _entries[it->second];
829 [[nodiscard]]
const entry& random_entry_(std::uint64_t call_seed,
bool weighted)
const
832 auto r = core::rng{call_seed}.split(
"country");
833 const auto idx = weighted ? _country_dist(r)
834 : static_cast<std::size_t>(core::bounded(r, _entries.size()));
835 return _entries[idx];
838 static unsigned days_in_(
int year,
unsigned month)
840 return static_cast<unsigned>(
841 std::chrono::year_month_day_last{std::chrono::year{year} / std::chrono::month{month} /
847 [[nodiscard]]
static bool after_reference_(
unsigned month,
unsigned day,
848 std::chrono::year_month_day reference)
850 const auto ref_m =
static_cast<unsigned>(reference.month());
851 const auto ref_d =
static_cast<unsigned>(reference.day());
852 return month > ref_m || (month == ref_m && day > ref_d);
864 [[nodiscard]]
static placed place_(core::rng& r,
const entry& e,
unsigned month,
unsigned age,
865 std::optional<year_t> fixed_year,
866 std::chrono::year_month_day reference)
868 const int ref_year =
static_cast<int>(reference.year());
869 auto draw = [&]() -> placed {
872 const int y = fixed_year->value;
873 return {.year = y, .day = core::uniform_int(r, 1U, days_in_(y, month))};
877 const auto d = core::uniform_int(r, 1U, 31U);
878 const int y = ref_year -
static_cast<int>(age) -
879 (after_reference_(month, d, reference) ? 1 : 0);
880 if (d <= days_in_(y, month))
return {.year = y, .day = d};
884 static constexpr int max_weekend_redraws = 3;
885 for (
int attempt = 0; attempt < max_weekend_redraws; ++attempt)
887 const std::chrono::weekday wd{std::chrono::sys_days{
888 std::chrono::year{p.year} / std::chrono::month{month} / std::chrono::day{p.day}}};
889 const bool weekend = wd.iso_encoding() >= 6;
890 if (!weekend || !core::bernoulli(r, core::math::fenced(e.csection_rate * 0.5)))
break;
896 [[nodiscard]] birth generate_(
const entry& e, std::uint64_t call_seed,
const birth_request& opts)
const
898 const core::rng call{call_seed};
900 b._signature = {.seed = call_seed, .data =
data_hash()};
901 b.country_code = e.cca2;
906 b.bio_sex = *opts.bio_sex;
910 auto r = call.split(
"sex");
911 const double female_share = e.total_female / (e.total_male + e.total_female);
912 b.bio_sex = core::bernoulli(r, female_share) ? sex::female : sex::male;
914 const bool male = b.bio_sex == sex::male;
920 auto r = call.split(
"age");
921 const auto& weights = male ? e.male_weights : e.female_weights;
922 const auto top =
static_cast<std::uint8_t
>(max_age_);
923 const auto range = opts.ages.value_or(age_range{.min = 0, .max = top});
924 const auto age_min = std::min(range.min, top);
925 const auto age_max = std::min(range.max, top);
926 if (age_min == 0 && age_max == max_age_)
928 age =
static_cast<unsigned>((male ? e.male_age : e.female_age)(r));
932 const std::vector<double> slice(weights.begin() + age_min,
933 weights.begin() + age_max + 1);
935 for (
const double w : slice) total += w;
936 const auto offset = total > 0.0 ? core::discrete{slice}(r)
937 : static_cast<std::size_t>(core::bounded(r, slice.size()));
938 age = age_min +
static_cast<unsigned>(offset);
944 auto r = call.split(
"month");
945 b.month =
static_cast<std::uint8_t
>(e.month(r) + 1);
950 auto r = call.split(
"day");
951 const auto p = place_(r, e, b.month, age, opts.year, opts.reference_date);
952 b.year =
static_cast<std::uint16_t
>(p.year);
953 b.day =
static_cast<std::uint8_t
>(p.day);
956 const int completed =
static_cast<int>(opts.reference_date.year()) - p.year -
957 (after_reference_(b.month, p.day, opts.reference_date) ? 1 : 0);
958 age =
static_cast<unsigned>(std::clamp(completed, 0,
static_cast<int>(max_age_)));
961 b.age =
static_cast<std::uint8_t
>(age);
964 const std::chrono::weekday wd{std::chrono::sys_days{
965 std::chrono::year{b.year} / std::chrono::month{b.month} / std::chrono::day{b.day}}};
966 b.weekday =
static_cast<std::uint8_t
>(wd.c_encoding());
967 const double le = male ? e.le_male : e.le_female;
968 b.le_remaining = std::max(0.0, le -
static_cast<double>(b.age));
969 b.cohort = std::string{cohort_label_(b.year)};
973 static std::string_view cohort_label_(
int year)
975 if (year <= 1927)
return "Greatest Generation";
976 if (year <= 1945)
return "Silent Generation";
977 if (year <= 1964)
return "Baby Boomer";
978 if (year <= 1980)
return "Generation X";
979 if (year <= 1996)
return "Millennial";
980 if (year <= 2012)
return "Generation Z";
981 return "Generation Alpha";