277 ocg() : _tables{make_tables_()} {}
282 ocg& operator=(
ocg&&) noexcept = default;
292 static ocg inst{auto_probe_tag{}};
309 return generate_(request, draw_seed_());
321 return generate_(request, call_seed);
336 const core::batch_options& options)
357 const core::batch_options& options,
358 std::uint64_t batch_seed)
const
360 return core::run_batch(options, batch_seed, [&](std::uint64_t item_seed)
377 template <
typename Sink>
379 std::uint64_t batch_seed, Sink&& sink)
const
381 core::for_each_batch(options, batch_seed,
382 [&](std::uint64_t item_seed) {
return get_occupation(request, item_seed); },
383 std::forward<Sink>(sink));
392 template <
typename Sink>
408 [[nodiscard]] std::size_t
country_count()
const {
return _model.codes.size(); }
412 [[nodiscard]] std::size_t
job_count()
const {
return _model.jobs.size(); }
420 [[nodiscard]]
const std::vector<std::string>&
unresolved()
const {
return _model.unresolved; }
434 void load(
const std::filesystem::path& dir)
437 if (!std::filesystem::is_directory(dir, ec) ||
438 !std::filesystem::is_regular_file(dir / file_name_(
table::status), ec))
442 apply_(read_directory_(dir), core::layer_mode::replace,
true);
456 [[nodiscard]]
bool load(dataset tier)
458 const std::string tier_dir{core::tier_name(tier)};
459 const auto marker = tier_dir +
"/" + std::string{file_name_(
table::status)};
460 const auto root = core::find_resources(library_name_, marker);
462 for (
const auto t : all_tables_)
464 docs.at(index_(t)) = core::load_resource(tier_dir +
"/" + std::string{file_name_(t)},
465 embedded_files_(), root);
468 apply_(std::move(docs), core::layer_mode::replace,
true);
484 void extend(
const std::filesystem::path& path, core::layer_mode mode = core::layer_mode::override)
487 if (std::filesystem::is_directory(path, ec))
489 apply_(read_directory_(path), mode,
false);
492 for (
const auto t : all_tables_)
494 if (path.filename() == file_name_(t))
496 (void)
extend(t, core::tsv_document::load(path), mode);
500 throw std::invalid_argument(
"extend: expected status.tsv, inactivity.tsv, education.tsv, isco.tsv, "
501 "shares.tsv, factors.tsv or occupations.tsv, got " + path.filename().string());
515 const core::layer_info&
extend(
table which, core::tsv_document doc,
516 core::layer_mode mode = core::layer_mode::override)
519 docs.at(index_(which)).emplace(std::move(doc));
520 apply_(std::move(docs), mode,
false);
521 return _tables.at(index_(which)).layers().back();
533 core::layer_mode mode = core::layer_mode::override)
535 return extend(which, core::tsv_document::parse(text, std::move(source)), mode);
541 [[nodiscard]]
const std::vector<core::layer_info>&
layers(
table which)
const
543 return _tables.at(index_(which)).layers();
553 for (
const auto& t : _tables)
555 any = any || !t.layers().empty();
556 h = core::hash_combine(h, t.data_hash());
564 static constexpr std::string_view library_name_ =
"occupation-generator";
565 static constexpr std::size_t table_count_ = 7;
566 static constexpr std::array<table, table_count_> all_tables_{
570 using documents = std::array<std::optional<core::tsv_document>, table_count_>;
571 using tables = std::array<core::layered_table, table_count_>;
573 static constexpr std::size_t index_(
table t) {
return static_cast<std::size_t
>(t); }
575 static constexpr std::string_view file_name_(
table t)
590 static core::table_schema schema_(
table t)
595 return {.key = {
"country_code",
"sex",
"band"},
596 .required = {
"employed",
"unemployed",
"inactive"},
597 .optional = {
"basis",
"year"}};
599 return {.key = {
"band"}, .required = {
"student",
"retired",
"other"}, .optional = {}};
601 return {.key = {
"country_code",
"sex",
"band"},
602 .required = {
"less_than_basic",
"basic",
"intermediate",
"advanced"},
603 .optional = {
"basis",
"year"}};
604 case table::isco:
return {.key = {
"code"}, .required = {
"label",
"major"}, .optional = {
"skill"}};
606 return {.key = {
"country_code",
"sex",
"code"}, .required = {
"share"}, .optional = {
"basis",
"year"}};
608 return {.key = {
"country_code",
"major",
"dimension",
"value"},
609 .required = {
"factor"},
610 .optional = {
"basis",
"year"}};
612 return {.key = {
"key"},
613 .required = {
"label",
"isco",
"weight"},
614 .optional = {
"unit",
"min_age",
"max_age",
"tags",
"skills",
"gear"}};
619 static tables make_tables_()
629 static constexpr int working_age_ = 15;
630 static constexpr int school_age_ = 6;
631 static constexpr std::array<education_level, 4> education_levels_{
632 education_level::less_than_basic, education_level::basic, education_level::intermediate,
633 education_level::advanced};
635 static constexpr std::array<int, 4> education_min_age_{0, 14, 17, 21};
641 [[nodiscard]]
bool contains(
int age)
const {
return age >= min_age && age <= max_age; }
647 std::array<double, 3> weights{};
653 std::array<double, 4> weights{};
656 struct inactivity_row
659 std::array<double, 3> weights{};
678 std::vector<std::string> skills;
679 std::vector<std::string> gear;
680 std::vector<std::string> tags;
681 std::size_t isco_index{0};
686 std::vector<std::pair<band, double>> age;
687 std::array<double, 4> education{1.0, 1.0, 1.0, 1.0};
692 std::array<std::vector<status_row>, 2> status;
693 std::array<std::vector<education_row>, 2> education;
694 std::array<core::weighted_table<std::size_t>, 2> shares;
699 std::map<std::string, country_data, std::less<>> countries;
700 std::vector<std::string> codes;
701 std::vector<inactivity_row> inactivity;
702 std::vector<isco_data> isco;
703 std::vector<job_data> jobs;
704 std::vector<std::vector<std::size_t>> jobs_of;
705 std::unordered_map<std::string, factor_set> factors;
706 std::vector<std::string> unresolved;
709 static std::string factor_key_(std::string_view country, std::string_view major)
712 k.reserve(country.size() + major.size() + 1);
713 k.append(country).push_back(
'\x1f');
718 static std::size_t sex_index_(
const core::tsv_row& row)
720 const auto s = row[
"sex"];
721 if (
const auto parsed = core::parse_sex(s))
return static_cast<std::size_t
>(*parsed);
722 throw row.error(
"sex", std::format(
"expected male or female, got '{}'", s));
726 static std::optional<int> parse_age_(std::string_view text)
728 if (text.empty() || text.size() > 3)
return std::nullopt;
730 for (
const char c : text)
732 if (c <
'0' || c >
'9')
return std::nullopt;
733 value = value * 10 + (c -
'0');
739 static band parse_band_(
const core::tsv_row& row, std::string_view column)
741 const auto text = row.get<std::string>(column);
742 const std::string_view view{text};
743 std::optional<band> parsed;
744 if (view.ends_with(
'+'))
746 if (
const auto lo = parse_age_(view.substr(0, view.size() - 1))) parsed = band{.min_age = *lo, .max_age = 255};
748 else if (
const auto dash = view.find(
'-'); dash != std::string_view::npos)
750 const auto lo = parse_age_(view.substr(0, dash));
751 const auto hi = parse_age_(view.substr(dash + 1));
752 if (lo && hi && *hi >= *lo) parsed = band{.min_age = *lo, .max_age = *hi};
756 throw row.error(column, std::format(
"expected an age band such as '15-24' or '65+', got '{}'", text));
761 static double non_negative_(
const core::tsv_row& row, std::string_view column)
763 const auto v = row.get<
double>(column);
764 if (!(v >= 0.0))
throw row.error(column,
"must not be negative");
768 static std::vector<std::string> split_list_(std::string_view text)
770 std::vector<std::string> out;
771 while (!text.empty())
773 const auto cut = text.find(
';');
774 const auto item = text.substr(0, cut);
775 if (!item.empty()) out.emplace_back(item);
776 if (cut == std::string_view::npos)
break;
777 text.remove_prefix(cut + 1);
782 static std::string check_country_(
const core::tsv_row& row)
784 auto cca2 = row.get<std::string>(
"country_code");
785 if (!core::is_alpha2(cca2))
throw row.error(
"country_code",
"expected two capital letters (ISO 3166-1 alpha-2)");
791 static std::tuple<std::string, std::size_t, status_row> parse_status_(
const core::tsv_row& row)
793 auto cca2 = check_country_(row);
795 s.ages = parse_band_(row,
"band");
796 s.weights = {non_negative_(row,
"employed"), non_negative_(row,
"unemployed"), non_negative_(row,
"inactive")};
797 if (!(s.weights[0] + s.weights[1] + s.weights[2] > 0.0))
throw row.error(
"employed",
"the three shares are all 0");
798 return {std::move(cca2), sex_index_(row), s};
801 static inactivity_row parse_inactivity_(
const core::tsv_row& row)
804 r.ages = parse_band_(row,
"band");
805 r.weights = {non_negative_(row,
"student"), non_negative_(row,
"retired"), non_negative_(row,
"other")};
806 if (!(r.weights[0] + r.weights[1] + r.weights[2] > 0.0))
throw row.error(
"student",
"the three shares are all 0");
810 static std::tuple<std::string, std::size_t, education_row> parse_education_(
const core::tsv_row& row)
812 auto cca2 = check_country_(row);
814 e.ages = parse_band_(row,
"band");
816 for (std::size_t i = 0; i < education_levels_.size(); ++i)
818 e.weights.at(i) = non_negative_(row,
to_string(education_levels_.at(i)));
819 total += e.weights.at(i);
821 if (!(total > 0.0))
throw row.error(
"less_than_basic",
"the four shares are all 0");
822 return {std::move(cca2), sex_index_(row), e};
825 static isco_data parse_isco_(
const core::tsv_row& row)
828 i.code = row.get<std::string>(
"code");
829 i.label = row.get<std::string>(
"label");
830 i.major = row.get<std::string>(
"major");
831 i.skill = row.get_or<std::string>(
"skill",
"");
838 std::size_t sex_index{0};
843 static share_data parse_share_(
const core::tsv_row& row)
846 s.country = check_country_(row);
847 s.sex_index = sex_index_(row);
848 s.code = row.get<std::string>(
"code");
849 s.share = non_negative_(row,
"share");
859 std::size_t level{0};
863 static factor_data parse_factor_(
const core::tsv_row& row)
866 f.country = check_country_(row);
867 f.major = row.get<std::string>(
"major");
868 const auto dimension = row[
"dimension"];
869 if (dimension ==
"age_band")
872 f.ages = parse_band_(row,
"value");
874 else if (dimension ==
"education")
877 const auto value = row[
"value"];
878 const auto it = std::ranges::find_if(education_levels_, [&](education_level l) {
return to_string(l) == value; });
879 if (it == education_levels_.end())
881 throw row.error(
"value", std::format(
"expected less_than_basic, basic, intermediate or advanced, got '{}'", value));
883 f.level =
static_cast<std::size_t
>(it - education_levels_.begin());
887 throw row.error(
"dimension", std::format(
"expected age_band or education, got '{}'", dimension));
889 f.factor = non_negative_(row,
"factor");
893 struct occupation_data
899 static occupation_data parse_job_(
const core::tsv_row& row)
902 o.job.key = row.get<std::string>(
"key");
903 o.job.label = row.get<std::string>(
"label");
904 o.isco = row.get<std::string>(
"isco");
905 o.job.weight = non_negative_(row,
"weight");
906 o.job.unit = row.get_or<std::string>(
"unit",
"");
907 o.job.min_age = row.get_or<
int>(
"min_age", 0);
908 o.job.max_age = row.get_or<
int>(
"max_age", 255);
909 if (o.job.min_age < 0)
throw row.error(
"min_age",
"must not be negative");
910 if (o.job.max_age < o.job.min_age)
throw row.error(
"max_age",
"must not be below min_age");
911 o.job.skills = split_list_(row.get_or<std::string>(
"skills",
""));
912 o.job.gear = split_list_(row.get_or<std::string>(
"gear",
""));
913 o.job.tags = split_list_(row.get_or<std::string>(
"tags",
""));
917 static void validate_(
table t,
const core::tsv_row& row)
935 core::rng _engine{core::rng::from_entropy()};
937 struct auto_probe_tag {};
939 explicit ocg(auto_probe_tag ) : _tables{make_tables_()}
941 if (!
load(dataset::lite)) (void)
load(dataset::full);
946 static std::span<const core::embedded_file> embedded_files_()
948#ifdef DASMIG_OCCUPATION_EMBED
949 return embedded::occupationgen::files;
955 std::uint64_t draw_seed_() {
return _engine(); }
957 static documents read_directory_(
const std::filesystem::path& dir)
960 for (
const auto t : all_tables_)
962 const auto file = dir / file_name_(t);
964 if (std::filesystem::is_regular_file(file, ec))
966 docs.at(index_(t)) = core::tsv_document::load(file);
974 void apply_(documents docs, core::layer_mode mode,
bool reset)
976 tables next = _tables;
977 for (
const auto t : all_tables_)
979 auto& doc = docs.at(index_(t));
980 auto& tbl = next.at(index_(t));
983 if (reset) tbl.clear();
986 (void)tbl.extend(std::move(*doc), mode, [t](
const core::tsv_row& row) { validate_(t, row); });
988 auto built = build_model_(next);
989 _tables = std::move(next);
990 _model = std::move(built);
993 static model build_model_(
const tables& tbls)
996 for (
const auto& row : tbls.at(index_(
table::
status)).rows())
998 auto [cca2, si, s] = parse_status_(row);
999 m.countries[cca2].status.at(si).push_back(s);
1001 for (
const auto& [cca2, c] : m.countries)
1003 if (!c.status[0].empty() && !c.status[1].empty()) m.codes.push_back(cca2);
1008 auto [cca2, si, e] = parse_education_(row);
1009 m.countries[cca2].education.at(si).push_back(e);
1012 std::unordered_map<std::string, std::size_t> isco_index;
1013 for (
const auto& row : tbls.at(index_(
table::
isco)).rows())
1015 auto i = parse_isco_(row);
1016 isco_index[i.code] = m.isco.size();
1017 m.isco.push_back(std::move(i));
1019 m.jobs_of.resize(m.isco.size());
1021 for (
const auto& row : tbls.at(index_(
table::
shares)).rows())
1023 const auto s = parse_share_(row);
1024 const auto it = isco_index.find(s.code);
1025 if (it == isco_index.end())
1027 m.unresolved.push_back(std::format(
"{}:{}: unknown ISCO group '{}'", row.source(), row.line(), s.code));
1030 m.countries[s.country].shares.at(s.sex_index).add(it->second, s.share);
1032 for (
const auto& row : tbls.at(index_(
table::
factors)).rows())
1034 const auto f = parse_factor_(row);
1035 auto& set = m.factors[factor_key_(f.country, f.major)];
1036 if (f.is_age) set.age.emplace_back(f.ages, f.factor);
1037 else set.education.at(f.level) = f.factor;
1041 auto o = parse_job_(row);
1042 const auto it = isco_index.find(o.isco);
1043 if (it == isco_index.end())
1045 m.unresolved.push_back(std::format(
"{}:{}: unknown ISCO group '{}'", row.source(), row.line(), o.isco));
1048 o.job.isco_index = it->second;
1049 m.jobs_of.at(it->second).push_back(m.jobs.size());
1050 m.jobs.push_back(std::move(o.job));
1055 void require_data_()
const
1059 throw std::runtime_error(
"No occupation data loaded. Call load() first.");
1063 template <
class Row>
1064 static const Row* find_row_(
const std::vector<Row>& rows,
int age)
1066 const auto it = std::ranges::find_if(rows, [age](
const Row& r) {
return r.ages.contains(age); });
1067 return it == rows.end() ? nullptr : &*it;
1070 template <
class T, std::
size_t N>
1071 static T draw_(core::rng& r,
const std::array<double, N>& weights,
const std::array<T, N>& values)
1073 core::weighted_table<T> t;
1074 for (std::size_t i = 0; i < N; ++i) t.add(values.at(i), weights.at(i));
1078 [[nodiscard]]
employment_status draw_status_(
const country_data& c, std::size_t si,
int age, std::string_view cca2,
1081 if (age < school_age_)
return employment_status::inactive;
1082 if (age < working_age_)
return employment_status::student;
1083 const auto* row = find_row_(c.status.at(si), age);
1086 throw std::invalid_argument(std::format(
"No employment status data for age {} in {}.", age, cca2));
1088 const auto kind = draw_(r, row->weights,
1089 std::array{employment_status::employed, employment_status::unemployed,
1090 employment_status::inactive});
1091 if (kind != employment_status::inactive)
return kind;
1092 const auto* split = find_row_(_model.inactivity, age);
1093 if (split ==
nullptr)
return employment_status::inactive;
1094 return draw_(r, split->weights,
1095 std::array{employment_status::student, employment_status::retired, employment_status::inactive});
1098 [[nodiscard]]
static std::optional<education_level> draw_education_(
const country_data& c, std::size_t si,
int age,
1101 if (age < working_age_)
return std::nullopt;
1102 const auto* row = find_row_(c.education.at(si), age);
1103 if (row ==
nullptr)
return std::nullopt;
1105 std::array<double, 4> w = row->weights;
1106 for (std::size_t i = w.size() - 1; i > 0; --i)
1108 if (age < education_min_age_.at(i))
1110 w.at(i - 1) += w.at(i);
1114 return draw_(r, w, education_levels_);
1117 [[nodiscard]]
static bool job_open_(
const job_data& j,
int age)
1119 return j.weight > 0.0 && age >= j.min_age && age <= j.max_age;
1123 [[nodiscard]] job_info draw_job_(
const country_data& c, std::string_view cca2, std::size_t si,
int age,
1124 std::optional<education_level>
education,
const core::rng& call)
const
1128 const int job_age = std::max(age, working_age_);
1129 std::vector<double> factor(_model.isco.size(), 0.0);
1130 for (std::size_t i = 0; i < _model.isco.size(); ++i)
1132 const bool open = std::ranges::any_of(_model.jobs_of.at(i), [&](std::size_t j) {
1133 return job_open_(_model.jobs.at(j), age);
1135 if (!open)
continue;
1137 const auto it = _model.factors.find(factor_key_(cca2, _model.isco.at(i).major));
1138 if (it != _model.factors.end())
1140 for (
const auto& [ages, value] : it->second.age)
1142 if (ages.contains(job_age)) f *= value;
1148 auto rg = call.split(
"isco");
1149 const auto group = c.shares.at(si).try_pick_index(rg, [&factor](
const std::size_t& i,
double base) {
1150 return base * factor.at(i);
1154 throw std::invalid_argument(std::format(
"No occupation is open to a person aged {} in {}.", age, cca2));
1156 const std::size_t
isco = c.shares.at(si).entries()[*group].value;
1158 core::weighted_table<std::size_t> jobs;
1159 for (
const auto j : _model.jobs_of.at(
isco))
1161 if (job_open_(_model.jobs.at(j), age)) jobs.add(j, _model.jobs.at(j).weight);
1163 auto rj = call.split(
"job");
1164 const auto& d = _model.jobs.at(jobs.pick(rj));
1165 const auto& g = _model.isco.at(
isco);
1166 return {.key = d.key,
1169 .isco_label = g.label,
1177 [[nodiscard]] occupation generate_(
const occupation_request& req, std::uint64_t call_seed)
const
1180 const core::rng call{call_seed};
1182 std::string cca2{req.country};
1185 if (_model.codes.empty())
1187 throw std::invalid_argument(
"No country has employment status data for both sexes.");
1189 auto cr = call.split(
"country");
1190 cca2 = _model.codes[
static_cast<std::size_t
>(core::bounded(cr, _model.codes.size()))];
1192 const auto it = _model.countries.find(cca2);
1193 if (it == _model.countries.end())
1195 throw core::unknown_country_error(cca2);
1197 const auto& c = it->second;
1200 v._signature = {.seed = call_seed, .data =
data_hash()};
1201 v.country_code = cca2;
1205 v.person_sex = *req.person_sex;
1209 auto rs = call.split(
"sex");
1210 v.person_sex = core::bernoulli(rs, 0.5) ? sex::female : sex::male;
1212 const std::size_t si = v.person_sex == sex::male ? 0 : 1;
1213 const int age = req.age;
1217 v.status = *req.status;
1221 auto rt = call.split(
"status");
1222 v.status = draw_status_(c, si, age, cca2, rt);
1226 v.education = req.education;
1230 auto re = call.split(
"education");
1231 v.education = draw_education_(c, si, age, re);
1233 const bool preschool = age < school_age_ && v.status == employment_status::inactive;
1234 if (v.status != employment_status::student && !preschool)
1236 v.job = draw_job_(c, cca2, si, age, v.education, call);