259 static trtg inst{auto_probe_tag{}};
275 return generate_(request, draw_seed_());
286 std::uint64_t call_seed)
const
288 return generate_(request, call_seed);
303 const core::batch_options& options)
324 const core::batch_options& options,
325 std::uint64_t batch_seed)
const
327 return core::run_batch(options, batch_seed, [&](std::uint64_t item_seed)
344 template <
typename Sink>
346 std::uint64_t batch_seed, Sink&& sink)
const
348 core::for_each_batch(options, batch_seed,
349 [&](std::uint64_t item_seed) {
return get_personality(request, item_seed); },
350 std::forward<Sink>(sink));
359 template <
typename Sink>
371 [[nodiscard]]
bool has_data()
const {
return !_model.traits.empty(); }
375 [[nodiscard]] std::size_t
trait_count()
const {
return _model.traits.size(); }
379 [[nodiscard]] std::size_t
axis_count()
const {
return _model.axes.size(); }
384 [[nodiscard]]
bool has_trait(std::string_view key)
const
386 return _model.trait_index.contains(key);
394 [[nodiscard]]
const std::vector<std::string>&
unresolved()
const
396 return _model.unresolved;
411 void load(
const std::filesystem::path& dir)
414 if (!std::filesystem::is_directory(dir, ec) ||
415 !std::filesystem::is_regular_file(dir / file_name_(
table::traits), ec))
419 apply_(read_directory_(dir), core::layer_mode::replace,
true);
433 [[nodiscard]]
bool load(dataset tier)
435 const std::string tier_dir{core::tier_name(tier)};
436 const auto marker = tier_dir +
"/" + std::string{file_name_(
table::traits)};
437 const auto root = core::find_resources(library_name_, marker);
439 for (
const auto t : all_tables_)
441 docs.at(index_(t)) = core::load_resource(tier_dir +
"/" + std::string{file_name_(t)},
442 embedded_files_(), root);
445 apply_(std::move(docs), core::layer_mode::replace,
true);
460 void extend(
const std::filesystem::path& path, core::layer_mode mode = core::layer_mode::override)
463 if (std::filesystem::is_directory(path, ec))
465 apply_(read_directory_(path), mode,
false);
468 for (
const auto t : all_tables_)
470 if (path.filename() == file_name_(t))
472 (void)
extend(t, core::tsv_document::load(path), mode);
476 throw std::invalid_argument(
"extend: expected traits.tsv, axes.tsv, loadings.tsv or "
477 "relations.tsv, got " + path.filename().string());
486 const core::layer_info&
extend(
table which, core::tsv_document doc,
487 core::layer_mode mode = core::layer_mode::override)
490 docs.at(index_(which)).emplace(std::move(doc));
491 apply_(std::move(docs), mode,
false);
492 return _tables.at(index_(which)).layers().back();
504 core::layer_mode mode = core::layer_mode::override)
506 return extend(which, core::tsv_document::parse(text, std::move(source)), mode);
512 [[nodiscard]]
const std::vector<core::layer_info>&
layers(
table which)
const
514 return _tables.at(index_(which)).layers();
524 for (
const auto& t : _tables)
526 any = any || !t.layers().empty();
527 h = core::hash_combine(h, t.data_hash());
535 static constexpr std::string_view library_name_ =
"trait-generator";
536 static constexpr std::size_t table_count_ = 4;
537 static constexpr std::array<table, table_count_> all_tables_{
541 static constexpr double max_exponent_ = 30.0;
542 static constexpr double default_synergy_ = 2.0;
544 using documents = std::array<std::optional<core::tsv_document>, table_count_>;
545 using tables = std::array<core::layered_table, table_count_>;
547 static constexpr std::size_t index_(
table t) {
return static_cast<std::size_t
>(t); }
549 static constexpr std::string_view file_name_(
table t)
561 static core::table_schema schema_(
table t)
566 return {.key = {
"trait"}, .required = {
"label",
"weight"}, .optional = {
"group",
"tags"}};
567 case table::axes:
return {.key = {
"axis"}, .required = {
"label",
"mean",
"sd"}, .optional = {}};
568 case table::loadings:
return {.key = {
"trait",
"axis"}, .required = {
"loading"}, .optional = {}};
570 return {.key = {
"trait",
"other"}, .required = {
"relation"}, .optional = {
"factor"}};
575 static tables make_tables_()
584 personality::trait info;
596 enum class relation_kind : std::uint8_t
602 struct relation_entry
606 relation_kind kind{relation_kind::conflict};
612 static std::string label_(
const core::tsv_row& row)
614 auto label = row.get<std::string>(
"label");
615 if (label.empty())
throw row.error(
"label",
"must not be empty");
619 static trait_entry parse_trait_(
const core::tsv_row& row)
622 e.info.key = row.get<std::string>(
"trait");
623 e.info.label = label_(row);
624 e.weight = row.get<
double>(
"weight");
625 if (e.weight < 0.0)
throw row.error(
"weight",
"must not be negative");
626 e.info.group = row.get_or<std::string>(
"group",
"");
629 for (
const auto tag : row.list(
"tags"))
631 if (tag.empty())
throw row.error(
"tags",
"empty tag");
632 e.info.tags.emplace_back(tag);
638 static axis_entry parse_axis_(
const core::tsv_row& row)
640 axis_entry a{.key = row.get<std::string>(
"axis"),
641 .label = label_(row),
642 .mean = row.get<
double>(
"mean"),
643 .sd = row.get<
double>(
"sd")};
644 if (a.sd < 0.0)
throw row.error(
"sd",
"must not be negative");
648 static relation_entry parse_relation_(
const core::tsv_row& row)
650 relation_entry r{.trait = row.get<std::string>(
"trait"), .other = row.get<std::string>(
"other")};
651 if (r.trait == r.other)
throw row.error(
"other",
"a trait cannot relate to itself");
652 const auto kind = row.get<std::string_view>(
"relation");
653 if (kind ==
"conflict")
655 r.kind = relation_kind::conflict;
656 if (row.has(
"factor"))
throw row.error(
"factor",
"a conflict takes no factor");
658 else if (kind ==
"synergy")
660 r.kind = relation_kind::synergy;
661 r.factor = row.get_or<
double>(
"factor", default_synergy_);
662 if (!(r.factor > 0.0))
throw row.error(
"factor",
"must be positive");
666 throw row.error(
"relation",
"expected conflict or synergy, got '" + std::string{kind} +
"'");
671 static void validate_(
table t,
const core::tsv_row& row)
687 std::vector<trait_entry> traits;
688 std::vector<axis_entry> axes;
689 std::map<std::string, std::size_t, std::less<>> trait_index;
690 std::map<std::string, std::size_t, std::less<>> axis_index;
691 std::vector<std::vector<double>> loadings;
692 std::vector<std::vector<std::size_t>> conflicts;
693 std::vector<std::vector<std::pair<std::size_t, double>>> synergies;
694 core::weighted_table<std::size_t> weights;
695 std::vector<std::string> unresolved;
698 tables _tables{make_tables_()};
701 core::rng _engine{core::rng::from_entropy()};
703 struct auto_probe_tag {};
705 explicit trtg(auto_probe_tag )
707 if (!
load(dataset::lite)) (void)
load(dataset::full);
712 static std::span<const core::embedded_file> embedded_files_()
714#ifdef DASMIG_TRAIT_EMBED
715 return embedded::traitgen::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); });
752 auto built = build_model_(next);
753 _tables = std::move(next);
754 _model = std::move(built);
759 static std::string where_(
const core::tsv_row& row)
761 return std::format(
"{}:{}", row.source(), row.line());
766 static std::optional<std::size_t> resolve_trait_(model& m,
const core::tsv_row& row, std::string_view column)
768 const auto name = row[column];
769 const auto it = m.trait_index.find(name);
770 if (it != m.trait_index.end())
return it->second;
771 m.unresolved.push_back(std::format(
"{}: unknown trait '{}'", where_(row), name));
775 static std::optional<std::size_t> resolve_axis_(model& m,
const core::tsv_row& row)
777 const auto name = row[
"axis"];
778 const auto it = m.axis_index.find(name);
779 if (it != m.axis_index.end())
return it->second;
780 m.unresolved.push_back(std::format(
"{}: unknown axis '{}'", where_(row), name));
785 static void add_relations_(model& m,
const core::layered_table&
relations)
787 std::map<std::pair<std::string, std::string>, std::string> pairs;
790 const auto r = parse_relation_(row);
791 const auto [first, second] = std::minmax(r.trait, r.other);
792 const auto [it, inserted] = pairs.emplace(std::pair{first, second}, where_(row));
795 throw row.error(
"other", std::format(
"the relation between '{}' and '{}' is already "
796 "defined at {}", r.trait, r.other, it->second));
798 const auto a = resolve_trait_(m, row,
"trait");
799 const auto b = resolve_trait_(m, row,
"other");
800 if (!a || !b)
continue;
801 if (r.kind == relation_kind::conflict)
803 m.conflicts[*a].push_back(*b);
804 m.conflicts[*b].push_back(*a);
808 m.synergies[*a].emplace_back(*b, r.factor);
809 m.synergies[*b].emplace_back(*a, r.factor);
816 static void add_group_conflicts_(model& m)
818 std::map<std::string, std::vector<std::size_t>, std::less<>> groups;
819 for (std::size_t i = 0; i < m.traits.size(); ++i)
821 if (!m.traits[i].info.group.empty()) groups[m.traits[i].info.group].push_back(i);
823 for (
const auto& [name, members] : groups)
825 for (
const auto i : members)
827 std::ranges::copy_if(members, std::back_inserter(m.conflicts[i]),
828 [i](std::size_t j) { return j != i; });
831 for (
auto& c : m.conflicts)
833 std::ranges::sort(c);
834 const auto dup = std::ranges::unique(c);
835 c.erase(dup.begin(), dup.end());
839 static model build_model_(
const tables& t)
842 for (
const auto& row : t.at(index_(
table::
traits)).rows())
844 const std::size_t i = m.traits.size();
845 m.traits.push_back(parse_trait_(row));
846 m.trait_index.emplace(m.traits.back().info.key, i);
847 m.weights.add(i, m.traits.back().weight);
849 for (
const auto& row : t.at(index_(
table::
axes)).rows())
851 m.axis_index.emplace(row.get<std::string>(
"axis"), m.axes.size());
852 m.axes.push_back(parse_axis_(row));
854 m.loadings.assign(m.traits.size(), std::vector<double>(m.axes.size(), 0.0));
857 const auto ti = resolve_trait_(m, row,
"trait");
858 const auto ai = resolve_axis_(m, row);
859 if (ti && ai) m.loadings[*ti][*ai] = row.get<
double>(
"loading");
861 m.conflicts.resize(m.traits.size());
862 m.synergies.resize(m.traits.size());
864 add_group_conflicts_(m);
870 std::uint64_t draw_seed_() {
return _engine(); }
872 void require_data_()
const
874 if (_model.traits.empty())
876 throw std::runtime_error(
"No trait data loaded. Call load() first.");
884 explicit selection(
const model& m)
885 : _m{&m}, _blocked(m.
traits.size(), 0), _boost(m.
traits.size(), 1.0)
889 void block(std::size_t i) { _blocked[i] = 1; }
891 [[nodiscard]]
bool blocked(std::size_t i)
const {
return _blocked[i] != 0; }
893 void choose(std::size_t i)
897 for (
const auto c : _m->conflicts[i]) _blocked[c] = 1;
898 for (
const auto& [other, factor] : _m->synergies[i]) _boost[other] *= factor;
902 [[nodiscard]]
double weight(std::size_t i,
double base,
double affinity)
const
904 return blocked(i) ? 0.0 : core::math::fenced(base * affinity * _boost[i]);
907 std::vector<std::size_t> chosen;
911 std::vector<char> _blocked;
912 std::vector<double> _boost;
916 [[nodiscard]] selection start_(
const trait_request& request)
const
918 if (request.include.size() > request.count)
920 throw std::invalid_argument(std::format(
"{} traits included but only {} requested",
921 request.include.size(), request.count));
923 selection sel{_model};
924 for (
const auto& key : request.exclude)
926 const auto it = _model.trait_index.find(key);
927 if (it != _model.trait_index.end()) sel.block(it->second);
929 for (
const auto& key : request.include)
931 const auto it = _model.trait_index.find(key);
932 if (it == _model.trait_index.end())
934 throw std::invalid_argument(std::format(
"Unknown trait: {}", key));
936 if (sel.blocked(it->second))
938 throw std::invalid_argument(std::format(
939 "Included trait {} is repeated, excluded or conflicts with another included trait", key));
941 sel.choose(it->second);
947 std::vector<double> draw_axes_(
const core::rng& call, personality& p)
const
949 std::vector<double> z(_model.axes.size(), 0.0);
950 p.axes.reserve(_model.axes.size());
951 for (std::size_t a = 0; a < _model.axes.size(); ++a)
953 const auto& ax = _model.axes[a];
954 double base = ax.mean;
957 auto r = call.split(
"axis:" + ax.key);
958 base = core::normal(r, ax.mean, ax.sd);
959 z[a] = (base - ax.mean) / ax.sd;
961 p.axes.push_back({.key = ax.key, .label = ax.label, .base = base, .value = base});
967 [[nodiscard]] std::vector<double> affinities_(
const std::vector<double>& z)
const
969 std::vector<double> affinity(_model.traits.size(), 1.0);
970 for (std::size_t i = 0; i < affinity.size(); ++i)
973 for (std::size_t a = 0; a < z.size(); ++a) s += core::math::fenced(_model.loadings[i][a] * z[a]);
974 affinity[i] = core::math::exp(std::clamp(s, -max_exponent_, max_exponent_));
979 [[nodiscard]] personality generate_(
const trait_request& request, std::uint64_t call_seed)
const
982 auto sel = start_(request);
984 const core::rng call{call_seed};
986 p._signature = {.seed = call_seed, .data =
data_hash()};
987 const auto affinity = affinities_(draw_axes_(call, p));
990 auto r = call.split(
"traits");
991 while (sel.chosen.size() < request.count)
993 const auto pick = _model.weights.try_pick_index(
994 r, [&](
const std::size_t& i,
double w) {
return sel.weight(i, w, affinity[i]); });
1000 p.traits.reserve(sel.chosen.size());
1001 for (
const auto i : sel.chosen)
1003 p.traits.push_back(_model.traits[i].info);
1004 for (std::size_t a = 0; a < p.axes.size(); ++a) p.axes[a].value += _model.loadings[i][a];