204 intg() : _tables{make_tables_()} {}
219 static intg inst{auto_probe_tag{}};
234 return generate_(request, draw_seed_());
246 std::uint64_t call_seed)
const
248 return generate_(request, call_seed);
263 const core::batch_options& options)
284 const core::batch_options& options,
285 std::uint64_t batch_seed)
const
287 return core::run_batch(options, batch_seed, [&](std::uint64_t item_seed)
304 template <
typename Sink>
306 std::uint64_t batch_seed, Sink&& sink)
const
308 core::for_each_batch(options, batch_seed,
309 [&](std::uint64_t item_seed) {
return get_interest(request, item_seed); },
310 std::forward<Sink>(sink));
319 template <
typename Sink>
335 [[nodiscard]] std::size_t
country_count()
const {
return _model.countries.size(); }
342 for (
const auto& v : _model.items) n += v.size();
349 [[nodiscard]] std::span<const religion_share>
shares(std::string_view cca2)
const
351 const auto it = _model.countries.find(cca2);
352 if (it == _model.countries.end())
return {};
353 return it->second.shares;
362 [[nodiscard]]
const std::vector<std::string>&
unresolved()
const
364 return _model.unresolved;
380 void load(
const std::filesystem::path& dir)
383 if (!std::filesystem::is_directory(dir, ec) ||
384 !std::filesystem::is_regular_file(dir / file_name_(
table::religion), ec))
388 apply_(read_directory_(dir), core::layer_mode::replace,
true);
402 [[nodiscard]]
bool load(dataset tier)
404 const std::string tier_dir{core::tier_name(tier)};
405 const auto marker = tier_dir +
"/" + std::string{file_name_(
table::religion)};
406 const auto root = core::find_resources(library_name_, marker);
408 for (
const auto t : all_tables_)
410 docs.at(index_(t)) = core::load_resource(tier_dir +
"/" + std::string{file_name_(t)},
411 embedded_files_(), root);
414 apply_(std::move(docs), core::layer_mode::replace,
true);
430 void extend(
const std::filesystem::path& path, core::layer_mode mode = core::layer_mode::override)
433 if (std::filesystem::is_directory(path, ec))
435 apply_(read_directory_(path), mode,
false);
438 for (
const auto t : all_tables_)
440 if (path.filename() == file_name_(t))
442 (void)
extend(t, core::tsv_document::load(path), mode);
446 throw std::invalid_argument(
"extend: expected religion.tsv, interests.tsv, affinity.tsv, "
447 "age_bands.tsv or regions.tsv, got " + path.filename().string());
461 const core::layer_info&
extend(
table which, core::tsv_document doc,
462 core::layer_mode mode = core::layer_mode::override)
465 docs.at(index_(which)).emplace(std::move(doc));
466 apply_(std::move(docs), mode,
false);
467 return _tables.at(index_(which)).layers().back();
479 core::layer_mode mode = core::layer_mode::override)
481 return extend(which, core::tsv_document::parse(text, std::move(source)), mode);
487 [[nodiscard]]
const std::vector<core::layer_info>&
layers(
table which)
const
489 return _tables.at(index_(which)).layers();
499 for (
const auto& t : _tables)
501 any = any || !t.layers().empty();
502 h = core::hash_combine(h, t.data_hash());
510 static constexpr std::string_view library_name_ =
"interest-generator";
511 static constexpr std::size_t table_count_ = 5;
512 static constexpr std::array<table, table_count_> all_tables_{
515 using documents = std::array<std::optional<core::tsv_document>, table_count_>;
516 using tables = std::array<core::layered_table, table_count_>;
518 static constexpr std::size_t index_(
table t) {
return static_cast<std::size_t
>(t); }
520 static constexpr std::string_view file_name_(
table t)
533 static core::table_schema schema_(
table t)
538 return {.key = {
"country_code",
"religion"}, .required = {
"group",
"weight"}, .optional = {
"year",
"basis"}};
540 return {.key = {
"kind",
"key"},
541 .required = {
"label",
"weight"},
542 .optional = {
"class",
"category",
"tags",
"min_age",
"max_age"}};
544 return {.key = {
"target",
"dimension",
"value"}, .required = {
"factor"}, .optional = {
"basis"}};
545 case table::age_bands:
return {.key = {
"band"}, .required = {
"min_age",
"max_age"}, .optional = {}};
546 case table::regions:
return {.key = {
"country_code"}, .required = {
"region"}, .optional = {}};
551 static tables make_tables_()
560 static constexpr std::size_t kind_count_ = 5;
561 static constexpr std::array<std::string_view, kind_count_> kind_names_{
"hobby",
"film",
"game",
"book",
"music"};
562 static constexpr std::array<std::string_view, kind_count_> kind_streams_{
"hobby",
"film",
"game",
"book",
"music"};
564 enum class dimension : std::uint8_t { sex, age_band, region, country,
religion };
565 static constexpr std::array<std::string_view, 5> dimension_names_{
"sex",
"age_band",
"region",
"country",
"religion"};
566 static constexpr std::size_t dimension_count_ = dimension_names_.size();
569 static constexpr std::uint32_t none_ = std::numeric_limits<std::uint32_t>::max();
573 using target_factors = std::array<std::vector<std::pair<std::uint32_t, double>>, dimension_count_>;
580 std::string category;
595 std::vector<religion_share> shares;
596 core::weighted_table<std::size_t> weights;
601 std::map<std::string, country_data, std::less<>> countries;
602 std::vector<std::string> codes;
603 std::array<std::vector<item_data>, kind_count_> items;
607 std::array<std::map<std::string, std::uint32_t, std::less<>>, dimension_count_> value_ids;
608 std::vector<target_factors> targets;
609 std::array<std::vector<std::array<std::uint32_t, 2>>, kind_count_> item_targets;
611 std::array<core::weighted_table<std::size_t>, kind_count_> item_tables;
612 std::vector<band_data> bands;
613 std::map<std::string, std::string, std::less<>> region_of;
614 std::vector<std::string> unresolved;
617 static std::size_t kind_of_(
const core::tsv_row& row)
619 const auto name = row[
"kind"];
620 for (std::size_t i = 0; i < kind_count_; ++i)
622 if (kind_names_[i] == name)
return i;
624 throw row.error(
"kind", std::format(
"expected hobby, film, game, book or music, got '{}'", name));
627 static dimension dimension_of_(
const core::tsv_row& row)
629 const auto name = row[
"dimension"];
630 for (std::size_t i = 0; i < dimension_names_.size(); ++i)
632 if (dimension_names_[i] == name)
return static_cast<dimension
>(i);
634 throw row.error(
"dimension", std::format(
"expected sex, age_band, region, country or religion, got '{}'", name));
639 static std::pair<std::string, religion_share> parse_share_(
const core::tsv_row& row)
641 std::string cca2 = row.get<std::string>(
"country_code");
642 if (!core::is_alpha2(cca2))
throw row.error(
"country_code",
"expected two capital letters (ISO 3166-1 alpha-2)");
644 s.religion = row.get<std::string>(
"religion");
645 s.group = row.get<std::string>(
"group");
646 s.weight = row.get<
double>(
"weight");
647 if (!(s.weight >= 0.0))
throw row.error(
"weight",
"must not be negative");
648 s.year = row.get_or<
int>(
"year", 0);
649 return {std::move(cca2), std::move(s)};
652 static std::pair<std::size_t, item_data> parse_item_(
const core::tsv_row& row)
654 const std::size_t kind = kind_of_(row);
656 i.key = row.get<std::string>(
"key");
657 i.label = row.get<std::string>(
"label");
658 i.weight = row.get<
double>(
"weight");
659 if (!(i.weight >= 0.0))
throw row.error(
"weight",
"must not be negative");
660 i.cls = row.get_or<std::string>(
"class",
"");
661 i.category = row.get_or<std::string>(
"category",
"");
662 i.min_age = row.get_or<
int>(
"min_age", 0);
663 i.max_age = row.get_or<
int>(
"max_age", 255);
664 if (i.min_age < 0)
throw row.error(
"min_age",
"must not be negative");
665 if (i.max_age < i.min_age)
throw row.error(
"max_age",
"must not be below min_age");
666 return {kind, std::move(i)};
672 dimension dim{dimension::sex};
677 static affinity_data parse_affinity_(
const core::tsv_row& row)
680 a.target = row.get<std::string>(
"target");
681 a.dim = dimension_of_(row);
682 a.value = row.get<std::string>(
"value");
683 if (a.dim == dimension::sex && !core::parse_sex(a.value))
685 throw row.error(
"value", std::format(
"expected male or female, got '{}'", a.value));
687 a.factor = row.get<
double>(
"factor");
688 if (!(a.factor >= 0.0))
throw row.error(
"factor",
"must not be negative");
692 static band_data parse_band_(
const core::tsv_row& row)
695 b.band = row.get<std::string>(
"band");
696 b.min_age = row.get<
int>(
"min_age");
697 b.max_age = row.get<
int>(
"max_age");
698 if (b.min_age < 0)
throw row.error(
"min_age",
"must not be negative");
699 if (b.max_age < b.min_age)
throw row.error(
"max_age",
"must not be below min_age");
703 static std::pair<std::string, std::string> parse_region_(
const core::tsv_row& row)
705 return {row.get<std::string>(
"country_code"), row.get<std::string>(
"region")};
708 static void validate_(
table t,
const core::tsv_row& row)
724 core::rng _engine{core::rng::from_entropy()};
726 struct auto_probe_tag {};
728 explicit intg(auto_probe_tag ) : _tables{make_tables_()}
730 if (!
load(dataset::lite)) (void)
load(dataset::full);
735 static std::span<const core::embedded_file> embedded_files_()
737#ifdef DASMIG_INTEREST_EMBED
738 return embedded::interestgen::files;
744 std::uint64_t draw_seed_() {
return _engine(); }
746 static documents read_directory_(
const std::filesystem::path& dir)
749 for (
const auto t : all_tables_)
751 const auto file = dir / file_name_(t);
753 if (std::filesystem::is_regular_file(file, ec))
755 docs.at(index_(t)) = core::tsv_document::load(file);
763 void apply_(documents docs, core::layer_mode mode,
bool reset)
765 tables next = _tables;
766 for (
const auto t : all_tables_)
768 auto& doc = docs.at(index_(t));
769 auto& tbl = next.at(index_(t));
772 if (reset) tbl.clear();
775 (void)tbl.extend(std::move(*doc), mode, [t](
const core::tsv_row& row) { validate_(t, row); });
777 auto built = build_model_(next);
778 _tables = std::move(next);
779 _model = std::move(built);
782 static model build_model_(
const tables& tbls)
785 for (
const auto& row : tbls.at(index_(
table::
religion)).rows())
787 auto [cca2, share] = parse_share_(row);
788 auto& c = m.countries[cca2];
789 c.weights.add(c.shares.size(), share.weight);
790 c.shares.push_back(std::move(share));
792 for (
const auto& [cca2, c] : m.countries)
794 if (c.weights.total_weight() > 0.0) m.codes.push_back(cca2);
797 std::map<std::string, bool, std::less<>> known;
800 auto [kind, item] = parse_item_(row);
801 known[item.key] =
true;
802 if (!item.cls.empty()) known[
"class:" + item.cls] =
true;
803 m.item_tables.at(kind).add(m.items.at(kind).size(), item.weight);
804 m.items.at(kind).push_back(std::move(item));
808 std::map<std::string, std::array<std::map<std::string, double, std::less<>>, dimension_count_>, std::less<>>
810 for (
const auto& row : tbls.at(index_(
table::
affinity)).rows())
812 const auto a = parse_affinity_(row);
813 if (!known.contains(a.target))
815 m.unresolved.push_back(std::format(
"{}:{}: unknown target '{}'", row.source(), row.line(), a.target));
818 affinity[a.target].at(
static_cast<std::size_t
>(a.dim))[a.value] = a.factor;
820 std::map<std::string_view, std::uint32_t, std::less<>> target_ids;
821 for (
const auto& [target, dims] :
affinity)
823 target_ids.emplace(target,
static_cast<std::uint32_t
>(m.targets.size()));
824 auto& factors = m.targets.emplace_back();
825 for (std::size_t d = 0; d < dimension_count_; ++d)
827 auto& ids = m.value_ids.at(d);
828 for (
const auto& [value, factor] : dims.at(d))
830 const auto id = ids.try_emplace(value,
static_cast<std::uint32_t
>(ids.size())).first->second;
831 factors.at(d).emplace_back(
id, factor);
835 const auto target_id = [&](std::string_view target) {
836 const auto it = target_ids.find(target);
837 return it == target_ids.end() ? none_ : it->second;
839 for (std::size_t k = 0; k < kind_count_; ++k)
841 for (
const auto& item : m.items.at(k))
843 m.item_targets.at(k).push_back(
844 {target_id(item.key), item.cls.empty() ? none_ : target_id(
"class:" + item.cls)});
847 for (
const auto& row : tbls.at(index_(
table::
age_bands)).rows()) m.bands.push_back(parse_band_(row));
848 for (
const auto& row : tbls.at(index_(
table::
regions)).rows())
850 auto [cca2, region] = parse_region_(row);
851 m.region_of[cca2] = std::move(region);
856 void require_data_()
const
860 throw std::runtime_error(
"No interest data loaded. Call load() first.");
871 std::string religion;
872 std::optional<int> age;
873 std::array<std::uint32_t, dimension_count_> ids{};
879 void resolve_ids_(person& p)
const
881 const auto id = [
this](dimension d, std::string_view value,
bool empty_matches) {
882 if (value.empty() && !empty_matches)
return none_;
883 const auto& ids = _model.value_ids.at(
static_cast<std::size_t
>(d));
884 const auto it = ids.find(value);
885 return it == ids.end() ? none_ : it->second;
887 p.ids = {id(dimension::sex, p.sex,
false), id(dimension::age_band, p.band,
false),
888 id(dimension::region, p.region,
false), id(dimension::country, p.cca2,
true),
889 id(dimension::religion, p.religion,
false)};
892 [[nodiscard]]
static const double* find_factor_(
const target_factors& t, dimension d,
const person& p)
894 const auto i =
static_cast<std::size_t
>(d);
895 const auto id = p.ids.at(i);
896 if (
id == none_)
return nullptr;
897 for (
const auto& [value, factor] : t.at(i))
899 if (value ==
id)
return &factor;
909 [[nodiscard]]
double item_weight_(std::size_t kind, std::size_t index,
const person& p)
const
911 double w = _model.items.at(kind)[index].weight;
912 for (
const auto t : _model.item_targets.at(kind)[index])
914 if (t == none_)
continue;
915 const auto& factors = _model.targets[t];
916 for (
const auto d : {dimension::sex, dimension::age_band, dimension::religion})
918 if (
const auto* f = find_factor_(factors, d, p)) w *= *f;
920 if (
const auto* c = find_factor_(factors, dimension::country, p)) w *= *c;
921 else if (
const auto* r = find_factor_(factors, dimension::region, p)) w *= *r;
931 void draw_items_(std::size_t kind, std::size_t n,
const person& p, core::rng& r,
932 std::vector<double>& weights, std::vector<interest_item>& out)
const
935 const auto& items = _model.items.at(kind);
936 weights.assign(items.size(), 0.0);
937 for (std::size_t i = 0; i < items.size(); ++i)
939 const auto& item = items[i];
940 if (p.age && (*p.age < item.min_age || *p.age > item.max_age))
continue;
941 weights[i] = item_weight_(kind, i, p);
943 const auto&
table = _model.item_tables.at(kind);
944 const auto adjust = [&weights](
const std::size_t& i,
double ) {
return weights[i]; };
945 for (std::size_t k = 0; k < n; ++k)
947 const auto picked =
table.try_pick_index(r, adjust);
949 const std::size_t i =
table.entries()[*picked].value;
951 out.push_back({.key = items[i].key, .label = items[i].label, .category = items[i].category});
955 [[nodiscard]] interest generate_(
const interest_request& req, std::uint64_t call_seed)
const
958 const core::rng call{call_seed};
960 std::string cca2{req.country};
963 if (_model.codes.empty())
965 throw std::invalid_argument(
"No country has a religion share above 0.");
967 auto cr = call.split(
"country");
968 cca2 = _model.codes[
static_cast<std::size_t
>(core::bounded(cr, _model.codes.size()))];
970 const auto it = _model.countries.find(cca2);
971 if (it == _model.countries.end())
973 throw core::unknown_country_error(cca2);
975 const auto& c = it->second;
976 if (!(c.weights.total_weight() > 0.0))
978 throw std::invalid_argument(std::format(
"Every religion share of {} has weight 0.", cca2));
981 auto rr = call.split(
"religion");
982 const auto& share = c.shares[c.weights.pick(rr)];
985 v._signature = {.seed = call_seed, .data =
data_hash()};
986 v.country_code = cca2;
987 v.religion = share.religion;
988 v.religion_group = share.group;
992 p.religion = share.group;
993 p.age = req.age ? std::optional<int>{*req.age} : std::nullopt;
994 if (
const auto rg = _model.region_of.find(cca2); rg != _model.region_of.end()) p.region = rg->second;
995 if (req.person_sex) p.sex = std::string{core::sex_name(*req.person_sex)};
996 if (p.age && !_model.bands.empty())
998 const auto b = std::ranges::find_if(_model.bands, [&](
const band_data& x) {
999 return *p.age >= x.min_age && *p.age <= x.max_age;
1001 if (b != _model.bands.end()) p.band = b->band;
1003 else if (std::ranges::all_of(_model.bands, [&](
const band_data& x) { return *p.age < x.min_age; }))
1010 std::array<std::vector<interest_item>*, kind_count_> out{&v.hobbies, &v.films, &v.games, &v.books, &v.music};
1011 std::vector<double> weights;
1012 for (std::size_t k = 0; k < kind_count_; ++k)
1014 auto r = call.split(kind_streams_[k]);
1015 draw_items_(k, k == 0 ? req.hobbies : req.media, p, r, weights, *out.at(k));