diff --git a/cxx/BUILD b/cxx/BUILD index 3b0a75e..7474d1d 100644 --- a/cxx/BUILD +++ b/cxx/BUILD @@ -65,9 +65,9 @@ cc_library( ":irm", ":observations", "//distributions:crp", + "//pclean:get_joint_relations", "//pclean:io", "//pclean:schema", - "//pclean:schema_helper", ], ) diff --git a/cxx/clean_relation.hh b/cxx/clean_relation.hh index 174a06e..38aecab 100644 --- a/cxx/clean_relation.hh +++ b/cxx/clean_relation.hh @@ -3,6 +3,7 @@ #pragma once +#include #include #include #include @@ -159,7 +160,10 @@ class CleanRelation : public Relation { } std::vector get_cluster_assignment(const T_items& items) const { - assert(items.size() == domains.size()); + if (items.size() != domains.size()) { + printf("Warning: for relation %s, items.size=%ld and domains.size()=%ld\n", name.c_str(), items.size(), domains.size()); + std::exit(1); + } std::vector z(domains.size()); for (int i = 0; i < std::ssize(domains); ++i) { z[i] = domains[i]->get_cluster_assignment(items[i]); diff --git a/cxx/distributions/stringcat.cc b/cxx/distributions/stringcat.cc index 38200d9..8914cd5 100644 --- a/cxx/distributions/stringcat.cc +++ b/cxx/distributions/stringcat.cc @@ -2,6 +2,7 @@ // See LICENSE.txt #include +#include #include #include #include "distributions/stringcat.hh" @@ -10,7 +11,8 @@ int StringCat::string_to_index(const std::string& s) const { auto it = std::find(strings.begin(), strings.end(), s); if (it == strings.end()) { - assert(false); + printf("String %s not in StringCat's list of strings\n", s.c_str()); + std::exit(1); } return it - strings.begin(); } diff --git a/cxx/gendb.cc b/cxx/gendb.cc index 8774947..66fb336 100644 --- a/cxx/gendb.cc +++ b/cxx/gendb.cc @@ -12,17 +12,19 @@ #include "hirm.hh" #include "irm.hh" #include "observations.hh" +#include "pclean/get_joint_relations.hh" #include "pclean/schema.hh" -#include "pclean/schema_helper.hh" GenDB::GenDB(std::mt19937* prng, const PCleanSchema& schema_, bool _only_final_emissions, bool _record_class_is_clean) - : schema(schema_), - schema_helper(schema_, _only_final_emissions, _record_class_is_clean) { - std::map> - annotated_domains_for_relation; - T_schema hirm_schema = - schema_helper.make_hirm_schema(&annotated_domains_for_relation); + : schema(schema_), only_final_emissions(_only_final_emissions), + record_class_is_clean(_record_class_is_clean) { + // Note that the domains cache must be populated before the reference + // indices. + compute_domains_cache(); + compute_reference_indices_cache(); + + T_schema hirm_schema = make_hirm_schema(); hirm = new HIRM(hirm_schema, prng); for (const auto& [class_name, unused_class] : schema.classes) { @@ -171,6 +173,7 @@ T_items GenDB::sample_class_ancestors(std::mt19937* prng, const std::string& class_name, int class_item, bool new_rows_have_unique_entities) { T_items items; + assert(schema.classes.contains(class_name)); PCleanClass c = schema.classes.at(class_name); for (const auto& [name, var] : c.vars) { @@ -203,7 +206,7 @@ void GenDB::get_relation_items(const std::string& rel_name, const int ind, const std::vector& domains = std::visit( [&](auto tr) { return tr.domains; }, hirm->schema.at(rel_name)); items[ind] = class_item; - auto& ref_indices = schema_helper.relation_reference_indices; + auto& ref_indices = relation_reference_indices; if (ref_indices.contains(rel_name)) { if (ref_indices.at(rel_name).contains(ind)) { for (const auto& [rf_name, rf_ind] : ref_indices.at(rel_name).at(ind)) { @@ -238,7 +241,7 @@ GenDB::unincorporate_reference(const std::string& class_name, std::vector domain_inds; for (size_t i = 0; i < domains.size(); ++i) { if (domains[i] == class_name && - schema_helper.relation_reference_indices.at(rel_name).at(i).contains( + relation_reference_indices.at(rel_name).at(i).contains( ref_field)) { domain_inds.push_back(i); } @@ -392,3 +395,220 @@ void GenDB::incorporate_reference_relation( } GenDB::~GenDB() { delete hirm; } + +void GenDB::compute_domains_cache() { + for (const auto& c : schema.classes) { + if (!domains.contains(c.first)) { + compute_domains_for(c.first); + } + } +} + +void GenDB::compute_reference_indices_cache() { + for (const auto& c : schema.classes) { + if (!class_reference_indices.contains(c.first)) { + compute_reference_indices_for(c.first); + } + } +} + +void GenDB::compute_domains_for(const std::string& name) { + std::vector ds; + assert(schema.classes.contains(name)); + PCleanClass c = schema.classes.at(name); + + for (const auto& v : c.vars) { + if (const ClassVar* cv = std::get_if(&(v.second.spec))) { + if (!domains.contains(cv->class_name)) { + compute_domains_for(cv->class_name); + } + for (const std::string& s : domains[cv->class_name]) { + ds.push_back(s); + } + } + } + + // Put the "primary" domain last, so that it survives reordering. + ds.push_back(name); + + domains[name] = ds; +} + +void GenDB::compute_reference_indices_for( + const std::string& name) { + std::vector ds; + int total_offset = 0; + assert(schema.classes.contains(name)); + PCleanClass c = schema.classes.at(name); + + // Recursively maps the indices of class "name" (and ancestors) in relation + // items to the names and indices (in items) of their parents (reference + // fields). + std::map> ref_indices; + + // Temporarily stores reference fields and indices for class "name"; + std::map class_ref_indices; + for (const auto& v : c.vars) { + if (const ClassVar* cv = std::get_if(&(v.second.spec))) { + if (!class_reference_indices.contains(cv->class_name)) { + compute_reference_indices_for(cv->class_name); + } + // Indices for foreign-key domains are generated by adding an offset + // to their indices in the respective class. + const int offset = total_offset; + total_offset += domains.at(cv->class_name).size(); + class_ref_indices[v.first] = total_offset - 1; + std::map child_class_indices; + if (class_reference_indices.contains(cv->class_name)) { + for (const auto& [ind, ref] : + class_reference_indices.at(cv->class_name)) { + std::map class_ref_indices; + for (const auto& [field_name, ref_ind] : ref) { + child_class_indices[field_name] = ref_ind + offset; + } + ref_indices[ind + offset] = child_class_indices; + } + } + } + } + + // Do not store a `class_reference_indices` entry for classes + // with no reference fields. + if (class_ref_indices.size() > 0) { + ref_indices[total_offset] = class_ref_indices; + class_reference_indices[name] = ref_indices; + } +} + +void GenDB::make_relations_for_queryfield( + const QueryField& f, const PCleanClass& record_class, T_schema* tschema) { + + // First, find all the vars and classes specified in f.class_path. + std::vector var_names; + std::vector class_names; + PCleanVariable last_var; + PCleanClass last_class = record_class; + class_names.push_back(record_class.name); + for (size_t i = 0; i < f.class_path.size(); ++i) { + const PCleanVariable& v = last_class.vars[f.class_path[i]]; + last_var = v; + var_names.push_back(v.name); + if (i < f.class_path.size() - 1) { + class_names.push_back(std::get(v.spec).class_name); + last_class = schema.classes.at(class_names.back()); + } + } + // Remove the last var_name because it isn't used in making the path_prefix. + var_names.pop_back(); + + // Get the base relation from the last class and variable name. + std::string base_relation_name = class_names.back() + ":" + last_var.name; + + // Handle queries of the record class specially. + if (f.class_path.size() == 1) { + if (record_class_is_clean) { + // Just rename the existing clean relation and set it to be observed. + T_clean_relation cr = + std::get(tschema->at(base_relation_name)); + cr.is_observed = true; + (*tschema)[f.name] = cr; + tschema->erase(base_relation_name); + } else { + T_noisy_relation tnr = + get_emission_relation(std::get(last_var.spec), + domains[record_class.name], base_relation_name); + tnr.is_observed = true; + (*tschema)[f.name] = tnr; + // If the record class is the only class in the schema, there will be + // no entries in `relation_reference_indices`. + if (class_reference_indices.contains(record_class.name)) { + relation_reference_indices[f.name] = + class_reference_indices.at(record_class.name); + } + } + return; + } + + // Handle only_final_emissions == true. + if (only_final_emissions) { + std::vector noisy_domains = domains[class_names.back()]; + for (int i = class_names.size() - 2; i >= 0; --i) { + noisy_domains.push_back(class_names[i]); + relation_reference_indices[f.name][noisy_domains.size() - 1] + [var_names[i]] = noisy_domains.size() - 2; + } + T_noisy_relation tnr = get_emission_relation( + std::get(last_var.spec), noisy_domains, base_relation_name); + tnr.is_observed = true; + (*tschema)[f.name] = tnr; + // If the record class is the only class in the schema, there will be + // no entries in `relation_reference_indices`. + if (relation_reference_indices.contains(base_relation_name)) { + relation_reference_indices[f.name] = + relation_reference_indices.at(base_relation_name); + } + return; + } + + // Handle only_final_emissions == false. + std::string& previous_relation = base_relation_name; + std::vector current_domains = domains[class_names.back()]; + std::map> ref_indices; + for (int i = f.class_path.size() - 2; i >= 0; --i) { + current_domains.push_back(class_names[i]); + ref_indices[current_domains.size() - 1][var_names[i]] = + current_domains.size() - 2; + T_noisy_relation tnr = get_emission_relation( + std::get(last_var.spec), current_domains, previous_relation); + std::string rel_name; + if (i == 0) { + rel_name = f.name; + tnr.is_observed = true; + } else { + // Intermediate emissions have a name of the form + // "[Observing Class]::[QueryFieldName]" + rel_name = class_names[i] + "::" + f.name; + tnr.is_observed = false; + } + (*tschema)[rel_name] = tnr; + // Since noisy relations have the leftmost domains in common with their base + // relations, they share the reference indices with their base relations as + // well. + if (relation_reference_indices.contains(previous_relation)) { + relation_reference_indices[rel_name] = + relation_reference_indices.at(previous_relation); + } + relation_reference_indices[rel_name].merge(ref_indices); + previous_relation = rel_name; + } +} + +T_schema GenDB::make_hirm_schema() { + T_schema tschema; + + // For every scalar variable, make a clean relation with the name + // "[ClassName]:[VariableName]". + for (const auto& c : schema.classes) { + for (const auto& v : c.second.vars) { + std::string rel_name = c.first + ':' + v.first; + if (const ScalarVar* dv = std::get_if(&(v.second.spec))) { + tschema[rel_name] = get_distribution_relation(*dv, domains[c.first]); + if (class_reference_indices.contains(c.first)) { + relation_reference_indices[rel_name] = + class_reference_indices.at(c.first); + } + } + } + } + + // For every query field, make one or more relations by walking up + // the class_path. At least one of those relations will have name equal + // to the name of the QueryField. + const PCleanClass record_class = schema.classes.at(schema.query.record_class); + for (const auto& [unused_name, f] : schema.query.fields) { + make_relations_for_queryfield(f, record_class, &tschema); + } + + return tschema; +} + diff --git a/cxx/gendb.hh b/cxx/gendb.hh index c40f01d..fbb504e 100644 --- a/cxx/gendb.hh +++ b/cxx/gendb.hh @@ -10,27 +10,9 @@ #include "hirm.hh" #include "observations.hh" #include "pclean/schema.hh" -#include "pclean/schema_helper.hh" class GenDB { public: - const PCleanSchema& schema; - - // TODO(emilyaf): Merge PCleanSchemaHelper and GenDB. - PCleanSchemaHelper schema_helper; - - // This data structure contains entity sets and linkages. Semantics are - // map ref_val>>, - // where primary_key and ref_val are (integer) entity IDs. - std::map, int> reference_values; - - HIRM* hirm; // Owned by the GenDB instance. - - // Map keys are class names. Values are CRPs for latent entities, where the - // "tables" are entity IDs and the "customers" are unique identifiers of - // observations of that class. - std::map domain_crps; - GenDB(std::mt19937* prng, const PCleanSchema& schema, bool _only_final_emissions = false, bool _record_class_is_clean = true); @@ -110,6 +92,9 @@ class GenDB { const std::string& class_name, const std::string& ref_field, const int class_item, const int new_ref_val); + // Translate the PCleanSchema into an HIRM T_schema. + T_schema make_hirm_schema(); + // Incorporates the items and values from stored_values (generally an output // of update_reference_items). void incorporate_reference( @@ -134,4 +119,64 @@ class GenDB { // Disable copying. GenDB& operator=(const GenDB&) = delete; GenDB(const GenDB&) = delete; + + // The rest of these methods are conceptually private, but actually + // public for testing. + + // For each class in the schema, set domains[class_name] to + // domains[cv1:class] + domains[cv2:class] + .... + [class_name] + // where cv1, cv2, ... are the class variables inside class class_name + // and cvi:class is the class associated to that class variable. + // This list will be used as the domains list for any HIRM relation + // created from a variable in class class_name. + void compute_domains_cache(); + + // Compute domains[name], recursively calling itself for any classes c + // that name depends on. + void compute_domains_for(const std::string& name); + + // Compute the relation_reference_indices and class_reference_indices + // datastructures. See below for a description of those. + void compute_reference_indices_cache(); + + // Compute relation_reference_indices and class_reference_indices for + // class name, recursively calling itself for any classes c that name + // depends on. + void compute_reference_indices_for(const std::string& name); + + // Make the relations associated with QueryField f and put them into + // schema. + void make_relations_for_queryfield( + const QueryField& f, const PCleanClass& record_class, T_schema* schema); + + // Member variables + const PCleanSchema& schema; + + // This data structure contains entity sets and linkages. Semantics are + // map ref_val>>, + // where primary_key and ref_val are (integer) entity IDs. + std::map, int> reference_values; + + HIRM* hirm; // Owned by the GenDB instance. + + // Map keys are class names. Values are CRPs for latent entities, where the + // "tables" are entity IDs and the "customers" are unique identifiers of + // observations of that class. + std::map domain_crps; + + bool only_final_emissions; + bool record_class_is_clean; + std::map> domains; + + // Map keys are relation name, item index of a class, and reference field + // name. The values in the inner map are the item index of the reference + // class. (See tests for more intuition.) + std::map>> + relation_reference_indices; + + // Map keys are class name, item index of a class, and reference field + // name. The values in the inner map are the item index of the reference + // class. (See tests for more intuition.) + std::map>> + class_reference_indices; }; diff --git a/cxx/gendb_test.cc b/cxx/gendb_test.cc index fc87650..8ea1781 100644 --- a/cxx/gendb_test.cc +++ b/cxx/gendb_test.cc @@ -15,14 +15,17 @@ struct SchemaTestFixture { SchemaTestFixture() { std::stringstream ss(R"""( class School - name ~ string + name ~ string(maxlength=60) + degree_dist ~ categorical(k=100) class Physician school ~ School degree ~ stringcat(strings="MD PT NP DO PHD") + specialty ~ stringcat(strings="Family Med:Internal Med:Physical Therapy", delim=":") class City name ~ string + state ~ stringcat(strings="AL AK AZ AR CA CO CT DE DC FL GA HI ID IL IN IA KS KY LA ME MD MA MI MN MS MO MT NE NV NH NJ NM NY NC ND OH OK OR PA RI SC SD TN TX UT VT VA WA WV WI WY") class Practice city ~ City @@ -32,9 +35,11 @@ class Record location ~ Practice observe + physician.specialty as Specialty physician.school.name as School physician.degree as Degree location.city.name as City + location.city.name as State from Record )"""); [[maybe_unused]] bool ok = read_schema(ss, &schema); @@ -49,15 +54,30 @@ observe void setup_gendb(std::mt19937* prng, GenDB& gendb, bool new_rows_have_unique_entities) { std::map obs0 = { + {"Specialty", "Family Med"}, {"School", "Massachusetts Institute of Technology"}, {"Degree", "PHD"}, - {"City", "Cambrij"}}; + {"City", "Cambrij"}, + {"State", "WA"} + }; std::map obs1 = { - {"School", "MIT"}, {"Degree", "MD"}, {"City", "Cambridge"}}; + {"Specialty", "Internal Med"}, + {"School", "MIT"}, + {"Degree", "MD"}, + {"City", "Cambridge"}, + {"State", "MA"}}; std::map obs2 = { - {"School", "Tufts"}, {"Degree", "PT"}, {"City", "Boston"}}; + {"Specialty", "Physical Therapy"}, + {"School", "Tufts"}, + {"Degree", "PT"}, + {"City", "Boston"}, + {"State", "MA"}}; std::map obs3 = { - {"School", "Boston University"}, {"Degree", "PhD"}, {"City", "Boston"}}; + {"Specialty", "Internal Med"}, + {"School", "Boston University"}, + {"Degree", "PhD"}, + {"City", "Boston"}, + {"State", "MA"}}; int i = 0; while (i < 30) { @@ -85,7 +105,7 @@ void test_unincorporate_reference_helper(GenDB& gendb, auto unincorporated_items = gendb.unincorporate_reference( class_name, ref_field, class_item, from_cluster_only); - const auto& ref_indices = gendb.schema_helper.relation_reference_indices; + const auto& ref_indices = gendb.relation_reference_indices; for (const auto& [name, trel] : gendb.hirm->schema) { // Store the indices of the relation domains that refer to the class and // the reference class. @@ -350,6 +370,389 @@ BOOST_AUTO_TEST_CASE(test_update_reference_items) { } } +BOOST_AUTO_TEST_CASE(test_domains_cache) { + std::mt19937 prng; + GenDB gendb(&prng, schema); + + std::vector expected_domains = {"School"}; + BOOST_TEST(gendb.domains["School"] == expected_domains); + + expected_domains = {"School", "Physician"}; + BOOST_TEST(gendb.domains["Physician"] == expected_domains); + + expected_domains = {"City"}; + BOOST_TEST(gendb.domains["City"] == expected_domains); + + expected_domains = {"City", "Practice"}; + BOOST_TEST(gendb.domains["Practice"] == expected_domains); + + expected_domains = {"City", "Practice", "School", "Physician", "Record"}; + BOOST_TEST(gendb.domains["Record"] == expected_domains, tt::per_element()); + + auto& ref_indices = gendb.class_reference_indices; + + // The Practice, Physician, and Record classes have reference fields, so they + // should be included in the reference field index map. + BOOST_TEST(ref_indices.size() == 3); + + // For Physician and Practice, index 1 corresponds to the class itself, and + // index 0 corresponds to the reference class. + BOOST_TEST_REQUIRE(ref_indices.contains("Physician")); + BOOST_TEST(ref_indices.at("Physician").at(1).at("school") == 0); + BOOST_TEST(ref_indices.at("Practice").at(1).at("city") == 0); + + // For Record, index 4 corresponds to the class itself, which points to + // physician (index 3) and location (index 1). + BOOST_TEST_REQUIRE(ref_indices.contains("Record")); + BOOST_TEST(ref_indices.at("Record").at(4).at("physician") == 3); + BOOST_TEST(ref_indices.at("Record").at(4).at("location") == 1); + BOOST_TEST(ref_indices.at("Record").at(3).at("school") == 2); + BOOST_TEST(ref_indices.at("Record").at(1).at("city") == 0); +} + +BOOST_AUTO_TEST_CASE(test_domains_and_reference_cache_two_paths_same_source) { + std::stringstream ss(R"""( +class City + name ~ string + +class Person + birth_city ~ City + home_city ~ City + +observe + birth_city.name as BirthCity + home_city.name as HomeCity + from Person +)"""); + PCleanSchema schema; + [[maybe_unused]] bool ok = read_schema(ss, &schema); + assert(ok); + std::mt19937 prng; + GenDB gendb(&prng, schema); + + std::vector expected_domains = {"City", "City", "Person"}; + BOOST_TEST(gendb.domains["Person"] == expected_domains, tt::per_element()); + + auto& ref_indices = gendb.class_reference_indices; + + // Only the Person field has reference fields. + BOOST_TEST(ref_indices.size() == 1); + BOOST_TEST_REQUIRE(ref_indices.contains("Person")); + BOOST_TEST(ref_indices.at("Person").at(2).at("birth_city") == 0); + BOOST_TEST(ref_indices.at("Person").at(2).at("home_city") == 1); +} + +BOOST_AUTO_TEST_CASE(test_domains_and_reference_cache_diamond) { + std::stringstream ss(R"""( +class City + name ~ string + +class School + location ~ City + +class Practice + location ~ City + +class Physician + practice ~ Practice + school ~ School + +observe + practice.location.name as PracticeCity + school.location.name as SchoolCity + from Physician +)"""); + PCleanSchema schema; + [[maybe_unused]] bool ok = read_schema(ss, &schema); + assert(ok); + std::mt19937 prng; + GenDB gendb(&prng, schema); + + std::vector expected_domains = {"City", "Practice", "City", + "School", "Physician"}; + BOOST_TEST(gendb.domains["Physician"] == expected_domains, + tt::per_element()); + + auto& ref_indices = gendb.class_reference_indices; + + BOOST_TEST(ref_indices.size() == 3); + + // Physician (index 4) has a reference field "practice", which appears + // at index 1. Practice has a reference field "location", which appears + // at index 0. + BOOST_TEST(ref_indices.at("Physician").at(4).at("practice") == 1); + BOOST_TEST(ref_indices.at("Physician").at(1).at("location") == 0); + + // Physician (index 4) has a reference field "school", which appears + // at index 3. School has a reference field "location", which appears + // at index 2. + BOOST_TEST(ref_indices.at("Physician").at(4).at("school") == 3); + BOOST_TEST(ref_indices.at("Physician").at(3).at("location") == 2); + + BOOST_TEST(ref_indices.at("Practice").at(1).at("location") == 0); + BOOST_TEST(ref_indices.at("School").at(1).at("location") == 0); +} + +BOOST_AUTO_TEST_CASE(test_make_relations_for_queryfield) { + std::mt19937 prng; + GenDB gendb(&prng, schema); + T_schema tschema; + + PCleanClass query_class = schema.classes[schema.query.record_class]; + gendb.make_relations_for_queryfield(schema.query.fields["School"], + query_class, &tschema); + + BOOST_TEST(tschema.size() == 2); + BOOST_TEST(tschema.contains("School")); + BOOST_TEST(tschema.contains("Physician::School")); + BOOST_TEST(std::get(tschema["School"]).is_observed); + BOOST_TEST( + !std::get(tschema["Physician::School"]).is_observed); +} + +BOOST_AUTO_TEST_CASE(test_make_relations_for_queryfield_only_final_emissions) { + std::mt19937 prng; + GenDB gendb(&prng, schema, true); + T_schema tschema; + + PCleanClass query_class = schema.classes[schema.query.record_class]; + gendb.make_relations_for_queryfield(schema.query.fields["School"], + query_class, &tschema); + BOOST_TEST(tschema.size() == 1); + BOOST_TEST(tschema.contains("School")); +} + +BOOST_AUTO_TEST_CASE(test_make_hirm_schmea) { + std::mt19937 prng; + GenDB gendb(&prng, schema); + T_schema tschema = gendb.make_hirm_schema(); + + BOOST_TEST(tschema.contains("School:name")); + T_clean_relation cr = std::get(tschema["School:name"]); + BOOST_TEST(!cr.is_observed); + BOOST_TEST((cr.distribution_spec.distribution == DistributionEnum::bigram)); + std::vector expected_domains = {"School"}; + BOOST_TEST(cr.domains == expected_domains); + + BOOST_TEST(tschema.contains("School:degree_dist")); + T_clean_relation cr2 = + std::get(tschema["School:degree_dist"]); + BOOST_TEST( + (cr2.distribution_spec.distribution == DistributionEnum::categorical)); + BOOST_TEST(cr2.distribution_spec.distribution_args.contains("k")); + BOOST_TEST(cr2.domains == expected_domains); + + BOOST_TEST(tschema.contains("Physician:degree")); + T_clean_relation cr3 = + std::get(tschema["Physician:degree"]); + BOOST_TEST( + (cr3.distribution_spec.distribution == DistributionEnum::stringcat)); + std::vector expected_domains2 = {"School", "Physician"}; + BOOST_TEST(cr3.domains == expected_domains2); + + BOOST_TEST(tschema.contains("Physician:specialty")); + + BOOST_TEST(tschema.contains("City:name")); + T_clean_relation cr4 = std::get(tschema["City:name"]); + std::vector expected_domains3 = {"City"}; + BOOST_TEST(cr4.domains == expected_domains3); + + BOOST_TEST(tschema.contains("City:state")); + + BOOST_TEST(tschema.contains("Specialty")); + T_noisy_relation nr1 = std::get(tschema["Specialty"]); + BOOST_TEST(nr1.is_observed); + BOOST_TEST((nr1.emission_spec.emission == EmissionEnum::bigram_string)); + expected_domains = {"School", "Physician", "Record"}; + BOOST_TEST(nr1.domains == expected_domains, tt::per_element()); + + BOOST_TEST(tschema.contains("School")); + T_noisy_relation nr2 = std::get(tschema["School"]); + BOOST_TEST(nr2.is_observed); + BOOST_TEST((nr2.emission_spec.emission == EmissionEnum::bigram_string)); + expected_domains = {"School", "Physician", "Record"}; + BOOST_TEST(nr2.domains == expected_domains, tt::per_element()); + + BOOST_TEST(tschema.contains("Degree")); + T_noisy_relation nr3 = std::get(tschema["Degree"]); + BOOST_TEST(nr3.is_observed); + BOOST_TEST((nr3.emission_spec.emission == EmissionEnum::bigram_string)); + expected_domains = {"School", "Physician", "Record"}; + BOOST_TEST(nr3.domains == expected_domains, tt::per_element()); + + BOOST_TEST(tschema.contains("City")); + T_noisy_relation nr4 = std::get(tschema["City"]); + BOOST_TEST(nr4.is_observed); + BOOST_TEST((nr4.emission_spec.emission == EmissionEnum::bigram_string)); + expected_domains = {"City", "Practice", "Record"}; + BOOST_TEST(nr4.domains == expected_domains, tt::per_element()); + + BOOST_TEST(tschema.contains("State")); + T_noisy_relation nr5 = std::get(tschema["State"]); + BOOST_TEST(nr5.is_observed); + BOOST_TEST((nr5.emission_spec.emission == EmissionEnum::bigram_string)); + expected_domains = {"City", "Practice", "Record"}; + BOOST_TEST(nr5.domains == expected_domains, tt::per_element()); + + BOOST_TEST(tschema.contains("Physician::School")); + T_noisy_relation nr6 = + std::get(tschema["Physician::School"]); + BOOST_TEST(!nr6.is_observed); + expected_domains = {"School", "Physician"}; + BOOST_TEST(nr6.domains == expected_domains, tt::per_element()); + + BOOST_TEST(tschema.contains("Practice::City")); + T_noisy_relation nr7 = std::get(tschema["Practice::City"]); + BOOST_TEST(!nr7.is_observed); + expected_domains = {"City", "Practice"}; + BOOST_TEST(nr7.domains == expected_domains, tt::per_element()); + + BOOST_TEST(tschema.contains("Practice::State")); + T_noisy_relation nr8 = std::get(tschema["Practice::State"]); + BOOST_TEST(!nr8.is_observed); + expected_domains = {"City", "Practice"}; + BOOST_TEST(nr8.domains == expected_domains, tt::per_element()); + + auto& ref_indices = gendb.relation_reference_indices; + + // Practice (index 1) has a reference field "city", which appears + // at index 0. + BOOST_TEST(ref_indices.at("Practice::State").at(1).at("city") == 0); + + // Record (index 2) has a reference field "location", which appears + // at index 1 (and refers to Practice). Practice has a reference field + // "city", which appears at index 0. + BOOST_TEST(ref_indices.at("State").at(2).at("location") == 1); + BOOST_TEST(ref_indices.at("State").at(1).at("city") == 0); +} + +BOOST_AUTO_TEST_CASE(test_make_hirm_schema_only_final_emissions) { + std::mt19937 prng; + GenDB gendb(&prng, schema, true); + T_schema tschema = gendb.make_hirm_schema(); + + BOOST_TEST(tschema.contains("School:name")); + T_clean_relation cr = std::get(tschema["School:name"]); + BOOST_TEST(!cr.is_observed); + BOOST_TEST((cr.distribution_spec.distribution == DistributionEnum::bigram)); + std::vector expected_domains = {"School"}; + BOOST_TEST(cr.domains == expected_domains); + + BOOST_TEST(tschema.contains("School:degree_dist")); + T_clean_relation cr2 = + std::get(tschema["School:degree_dist"]); + BOOST_TEST( + (cr2.distribution_spec.distribution == DistributionEnum::categorical)); + BOOST_TEST(cr2.distribution_spec.distribution_args.contains("k")); + BOOST_TEST(cr2.domains == expected_domains); + + BOOST_TEST(tschema.contains("Physician:degree")); + T_clean_relation cr3 = + std::get(tschema["Physician:degree"]); + BOOST_TEST( + (cr3.distribution_spec.distribution == DistributionEnum::stringcat)); + std::vector expected_domains2 = {"School", "Physician"}; + BOOST_TEST(cr3.domains == expected_domains2); + + BOOST_TEST(tschema.contains("Physician:specialty")); + + BOOST_TEST(tschema.contains("City:name")); + T_clean_relation cr4 = std::get(tschema["City:name"]); + std::vector expected_domains3 = {"City"}; + BOOST_TEST(cr4.domains == expected_domains3); + + BOOST_TEST(tschema.contains("City:state")); + + BOOST_TEST(tschema.contains("Specialty")); + T_noisy_relation nr1 = std::get(tschema["Specialty"]); + BOOST_TEST(nr1.is_observed); + BOOST_TEST((nr1.emission_spec.emission == EmissionEnum::bigram_string)); + expected_domains = {"School", "Physician", "Record"}; + BOOST_TEST(nr1.domains == expected_domains, tt::per_element()); + + BOOST_TEST(tschema.contains("School")); + T_noisy_relation nr2 = std::get(tschema["School"]); + BOOST_TEST(nr2.is_observed); + BOOST_TEST((nr2.emission_spec.emission == EmissionEnum::bigram_string)); + expected_domains = {"School", "Physician", "Record"}; + BOOST_TEST(nr2.domains == expected_domains, tt::per_element()); + + BOOST_TEST(tschema.contains("Degree")); + T_noisy_relation nr3 = std::get(tschema["Degree"]); + BOOST_TEST(nr3.is_observed); + BOOST_TEST((nr3.emission_spec.emission == EmissionEnum::bigram_string)); + expected_domains = {"School", "Physician", "Record"}; + BOOST_TEST(nr3.domains == expected_domains, tt::per_element()); + + BOOST_TEST(tschema.contains("City")); + T_noisy_relation nr4 = std::get(tschema["City"]); + BOOST_TEST(nr4.is_observed); + BOOST_TEST((nr4.emission_spec.emission == EmissionEnum::bigram_string)); + expected_domains = {"City", "Practice", "Record"}; + BOOST_TEST(nr4.domains == expected_domains, tt::per_element()); + + BOOST_TEST(tschema.contains("State")); + T_noisy_relation nr5 = std::get(tschema["State"]); + BOOST_TEST(nr5.is_observed); + BOOST_TEST((nr5.emission_spec.emission == EmissionEnum::bigram_string)); + expected_domains = {"City", "Practice", "Record"}; + BOOST_TEST(nr5.domains == expected_domains, tt::per_element()); + + auto& ref_indices = gendb.relation_reference_indices; + BOOST_TEST(ref_indices.at("State").at(2).at("location") == 1); + BOOST_TEST(ref_indices.at("State").at(1).at("city") == 0); +} + +BOOST_AUTO_TEST_CASE(test_record_class_is_clean) { + std::stringstream ss2(R"""( +class Record + rent ~ real + +observe + rent as "Rent" + from Record +)"""); + PCleanSchema schema2; + [[maybe_unused]] bool ok = read_schema(ss2, &schema2); + assert(ok); + + std::mt19937 prng; + GenDB gendb(&prng, schema2, false, true); + T_schema tschema = gendb.make_hirm_schema(); + + BOOST_TEST(!tschema.contains("Record:rent")); + BOOST_TEST(tschema.contains("Rent")); + + T_clean_relation cr = std::get(tschema["Rent"]); + BOOST_TEST(cr.is_observed); +} + +BOOST_AUTO_TEST_CASE(test_record_class_is_dirty) { + std::stringstream ss2(R"""( +class Record + rent ~ real + +observe + rent as "Rent" + from Record +)"""); + PCleanSchema schema2; + [[maybe_unused]] bool ok = read_schema(ss2, &schema2); + assert(ok); + + std::mt19937 prng; + GenDB gendb(&prng, schema2, false, false); + T_schema tschema = gendb.make_hirm_schema(); + + BOOST_TEST(tschema.contains("Record:rent")); + BOOST_TEST(tschema.contains("Rent")); + + T_clean_relation cr = std::get(tschema["Record:rent"]); + BOOST_TEST(!cr.is_observed); + T_noisy_relation nr = std::get(tschema["Rent"]); + BOOST_TEST(nr.is_observed); +} + BOOST_AUTO_TEST_CASE(test_incorporate_stored_items) { std::mt19937 prng; GenDB gendb(&prng, schema); diff --git a/cxx/pclean/BUILD b/cxx/pclean/BUILD index 920c5e5..5b107dc 100644 --- a/cxx/pclean/BUILD +++ b/cxx/pclean/BUILD @@ -20,6 +20,7 @@ cc_library( name = "get_joint_relations", hdrs = ["get_joint_relations.hh"], srcs = ["get_joint_relations.cc"], + visibility = ["//:__subpackages__"], deps = [ ":schema", "//:clean_relation", @@ -64,8 +65,8 @@ cc_binary( ":io", ":pclean_lib", ":schema", - ":schema_helper", "//:cxxopts", + "//:gendb", "//:hirm_lib", "//:inference", "//:util_io", @@ -79,6 +80,7 @@ cc_library( deps = [ ":csv", ":schema", + "//:gendb", "//:hirm_lib", "//:util_io", ], @@ -90,7 +92,6 @@ cc_test( deps = [ ":io", ":pclean_lib", - ":schema_helper", "@boost//:test", ], ) @@ -101,26 +102,3 @@ cc_library( visibility = ["//:__subpackages__"], deps = [], ) - -cc_library( - name = "schema_helper", - hdrs = ["schema_helper.hh"], - srcs = ["schema_helper.cc"], - visibility = ["//:__subpackages__"], - deps = [ - ":get_joint_relations", - ":schema", - "//:irm", - ], -) - - -cc_test( - name = "schema_helper_test", - srcs = ["schema_helper_test.cc"], - deps = [ - ":io", - ":schema_helper", - "@boost//:test", - ], -) diff --git a/cxx/pclean/pclean.cc b/cxx/pclean/pclean.cc index 238cb81..3b9c005 100644 --- a/cxx/pclean/pclean.cc +++ b/cxx/pclean/pclean.cc @@ -9,6 +9,7 @@ #include #include "cxxopts.hpp" +#include "gendb.hh" #include "irm.hh" #include "hirm.hh" #include "inference.hh" @@ -17,7 +18,6 @@ #include "pclean/io.hh" #include "pclean/pclean_lib.hh" #include "pclean/schema.hh" -#include "pclean/schema_helper.hh" int main(int argc, char** argv) { cxxopts::Options options( @@ -70,16 +70,13 @@ int main(int argc, char** argv) { std::cout << "Error reading schema file" << schema_fn << "\n"; } - // Translate schema - std::cout << "Making schema helper ...\n"; - PCleanSchemaHelper schema_helper( + // Make GenDB + std::cout << "Making GenDB model ...\n"; + GenDB gendb( + &prng, pclean_schema, result["only_final_emissions"].as(), result["record_class_is_clean"].as()); - std::cout << "Translating schema ...\n"; - std::map> annotated_domains_for_relations; - T_schema hirm_schema = schema_helper.make_hirm_schema( - &annotated_domains_for_relations); // Read observations std::cout << "Reading observations ...\n"; @@ -87,51 +84,34 @@ int main(int argc, char** argv) { std::cout << "Reading observations file from " << obs_fn << "\n"; DataFrame df = DataFrame::from_csv(obs_fn); - // Create model - std::cout << "Creating hirm ...\n"; - HIRM hirm(hirm_schema, &prng); - // Incorporate observations. - std::cout << "Translating observations ...\n"; - T_observations observations = translate_observations( - df, hirm_schema, annotated_domains_for_relations); - - std::string heldout_fn = result["heldout"].as(); - T_observations heldout_obs; - T_observations encoding_observations; - if (heldout_fn.empty()) { - encoding_observations = observations; - } else { - std::cout << "Loading held out observations from " << heldout_fn << std::endl; - DataFrame heldout_df = DataFrame::from_csv(heldout_fn); - heldout_obs = translate_observations( - heldout_df, hirm_schema, annotated_domains_for_relations); - encoding_observations = merge_observations(observations, heldout_obs); - } - - std::cout << "Encoding observations ...\n"; - T_encoding encoding = calculate_encoding(hirm_schema, encoding_observations); - std::cout << "Incorporating observations ...\n"; - incorporate_observations(&prng, &hirm, encoding, observations); + incorporate_observations(&prng, &gendb, df); // Run inference std::cout << "Running inference ...\n"; - inference_hirm(&prng, &hirm, - result["iters"].as(), - result["timeout"].as(), - result["verbose"].as()); + inference_gendb(&prng, &gendb, + result["iters"].as(), + result["timeout"].as(), + result["verbose"].as()); // Save results if (result.count("output") > 0) { std::string out_fn = result["output"].as(); std::cout << "Savings results to " << out_fn << "\n"; - to_txt(out_fn, hirm, encoding); + // TODO(thomaswc): Fix this. + // to_txt(out_fn, gendb.hirm, encoding); } + std::string heldout_fn = result["heldout"].as(); if (!heldout_fn.empty()) { - double lp = logp(&prng, &hirm, encoding, heldout_obs); - std::cout << "Log likelihood of held out data is " << lp << std::endl; + std::cout << "Loading held out observations from " << heldout_fn << std::endl; + DataFrame heldout_df = DataFrame::from_csv(heldout_fn); + std::cout << "Incorporating held out observations ...\n"; + double lp1 = gendb.logp_score(); + incorporate_observations(&prng, &gendb, heldout_df); + double lp2 = gendb.logp_score(); + std::cout << "Log likelihood of held out data is " << (lp2 - lp1) << std::endl; } int num_samples = result["samples"].as(); @@ -139,8 +119,7 @@ int main(int argc, char** argv) { std::string samples_out = result["output"].as() + ".samples"; std::cout << "Generating " << num_samples << " samples\n"; DataFrame samples_df = make_pclean_samples( - num_samples, &hirm, pclean_schema, - annotated_domains_for_relations, &prng); + num_samples, df.data.begin()->second.size(), &gendb, &prng); std::cout << "Writing samples to " << samples_out << " ...\n"; samples_df.to_csv(samples_out); } diff --git a/cxx/pclean/pclean_lib.cc b/cxx/pclean/pclean_lib.cc index 3bc76f8..b3a40f7 100644 --- a/cxx/pclean/pclean_lib.cc +++ b/cxx/pclean/pclean_lib.cc @@ -9,25 +9,20 @@ #include "pclean/pclean_lib.hh" #include "pclean/schema.hh" -T_observations translate_observations( - const DataFrame& df, const T_schema &schema, - const std::map> - &annotated_domains_for_relations) { - T_observations obs; - - for (const auto& col : df.data) { - const std::string& col_name = col.first; - if (!schema.contains(col_name)) { - printf("Schema does not contain %s, skipping ...\n", col_name.c_str()); - continue; - } - - const T_relation& trel = schema.at(col_name); - size_t num_domains = std::visit([&](const auto &r) { - return r.domains.size();}, trel); - assert(num_domains == annotated_domains_for_relations.at(col_name).size()); - - for (size_t i = 0; i < col.second.size(); ++i) { +void incorporate_observations(std::mt19937* prng, + GenDB *gendb, + const DataFrame& df) { + int num_rows = df.data.begin()->second.size(); + for (int i = 0; i < num_rows; i++) { + std::map row_values; + for (const auto& col : df.data) { + const std::string& col_name = col.first; + if (!gendb->schema.query.fields.contains(col_name)) { + if (i == 0) { + printf("Schema does not contain %s, skipping ...\n", col_name.c_str()); + } + continue; + } const std::string& val = col.second[i]; if (val.empty()) { // Don't incorporate missing values. @@ -40,89 +35,52 @@ T_observations translate_observations( for (const char c: val) { if (!std::isprint(c)) { printf("Found non-printable character with ascii value %d on line " - "%ld of column %s in value `%s`.\n", - (int)c, i+2, col_name.c_str(), val.c_str()); + "%d of column %s in value `%s`.\n", + (int) c, i + 2, col_name.c_str(), val.c_str()); std::exit(1); } } - std::vector entities; - for (size_t j = 0; j < num_domains; ++j) { - // Give every row it's own universe of unique id's. - // TODO(thomaswc): Discuss other options for handling this, such - // as sampling the non-index domains from a CRP prior or specifying - // additional CSV columns to use as foreign keys. - entities.push_back(annotated_domains_for_relations.at(col_name)[j] - + ":" + std::to_string(i)); - } - obs[col_name].push_back(std::make_tuple(entities, val)); + + const RelationVariant& rv = gendb->hirm->get_relation(col_name); + ObservationVariant ov; + std::visit([&](const auto &r) { ov = r->from_string(val); }, rv); + row_values[col_name] = ov; } + // Incorporate into the gendb with new_rows_have_unique_entities=true. + // TODO(emilyaf): Consider using new_rows_have_unique_entities=false + // after entity transitions are allowed and numeric stability issues + // are addressed. + gendb->incorporate(prng, std::make_pair(i, row_values), true); } - return obs; } // Sample a single "row" into *query_values. A value is sampled into -// (*query_values)[f] for every query field in the schema. The samples -// are generated from the HIRM by first sampling an unique entity id for -// each annotated domain used by the query field relations from the HIRM's -// per-domain CRPs. -// TODO(thomaswc): Remember the entity id samples across rows, so that -// if we said that Person #5 was born in city #3, we remember that if -// Person #5 comes up again. -void WIP_make_pclean_sample( - HIRM *hirm, const PCleanSchema& schema, - const std::map> - &annotated_domains_for_relations, - std::mt19937* prng, +// (*query_values)[f] for every query field in the schema. +void make_pclean_sample( + std::mt19937* prng, GenDB* gendb, int class_item, std::map *query_values) { - std::map domain_crps; - hirm->initialize_domain_crps(&domain_crps); + for (const auto& [name, query_field] : gendb->schema.query.fields) { + T_items entities = gendb->sample_entities_relation( + prng, gendb->schema.query.record_class, + query_field.class_path.begin(), query_field.class_path.end(), + class_item, false); - // entity_assignments[annotated_entity] gives the entity id for that entity. - std::map entity_assignments; - for (const auto& [name, query_field] : schema.query.fields) { - T_items entities; - const std::vector& domains = std::visit( - [](auto trel) { return trel.domains; }, - hirm->schema[query_field.name]); - const std::vector& annotated_domains = - annotated_domains_for_relations.at(query_field.name); - if (domains.size() != annotated_domains.size()) { - printf("For relation %s, found %ld domains but %ld annotated domains\n", - query_field.name.c_str(), domains.size(), annotated_domains.size()); - std::exit(1); - } - for (size_t i = 0; i < domains.size(); ++i) { - int id = -1; - auto it = entity_assignments.find(annotated_domains[i]); - if (it == entity_assignments.end()) { - id = domain_crps[domains[i]].sample(prng); - int crp_item = domain_crps[domains[i]].assignments.size(); - domain_crps[domains[i]].incorporate(crp_item, id); - entity_assignments[annotated_domains[i]] = id; - } - else { - id = it->second; - } - entities.push_back(id); - } - (*query_values)[query_field.name] = hirm->sample_and_incorporate_relation( + (*query_values)[query_field.name] = gendb->hirm->sample_and_incorporate_relation( prng, query_field.name, entities); } } -DataFrame make_pclean_samples( - int num_samples, HIRM *hirm, const PCleanSchema& schema, - const std::map> - &annotated_domains_for_relations, - std::mt19937* prng) { +DataFrame make_pclean_samples(int num_samples, int start_row, GenDB *gendb, + std::mt19937* prng) { DataFrame df; for (int i = 0; i < num_samples; i++) { std::map query_values; - WIP_make_pclean_sample(hirm, schema, annotated_domains_for_relations, - prng, &query_values); + make_pclean_sample(prng, gendb, start_row + i, &query_values); for (const auto& [column, val] : query_values) { df.data[column].push_back(val); } + } return df; } + diff --git a/cxx/pclean/pclean_lib.hh b/cxx/pclean/pclean_lib.hh index b4299ae..63cc038 100644 --- a/cxx/pclean/pclean_lib.hh +++ b/cxx/pclean/pclean_lib.hh @@ -3,6 +3,7 @@ #pragma once +#include "gendb.hh" #include "irm.hh" #include "util_io.hh" #include "pclean/csv.hh" @@ -10,17 +11,14 @@ #include "pclean/schema.hh" // For each non-missing value in the DataFrame df, create an -// observation in the returned T_observations. The column name of the value +// observation and incorporate it into the GenDB. The column name of the value // is used as the relation name, and each entity in each domain is given // its own unique value. -T_observations translate_observations( - const DataFrame& df, const T_schema &schema, - const std::map> - &annotated_domains_for_relation); +void incorporate_observations(std::mt19937* prng, + GenDB *gendb, + const DataFrame& df); -// Return a dataframe of num_samples samples from the HIRM. -DataFrame make_pclean_samples( - int num_samples, HIRM *hirm, const PCleanSchema& schema, - const std::map> - &annotated_domains_for_relation, - std::mt19937* prng); +// Return a dataframe of num_samples samples from the GenDB. +// All existing rows added to gendb should have ids < start_row. +DataFrame make_pclean_samples(int num_samples, int start_row, GenDB *gendb, + std::mt19937* prng); diff --git a/cxx/pclean/pclean_lib_test.cc b/cxx/pclean/pclean_lib_test.cc index 637100e..03f928e 100644 --- a/cxx/pclean/pclean_lib_test.cc +++ b/cxx/pclean/pclean_lib_test.cc @@ -2,71 +2,126 @@ #include "pclean/io.hh" #include "pclean/pclean_lib.hh" -#include "pclean/schema_helper.hh" #include #include namespace tt = boost::test_tools; -BOOST_AUTO_TEST_CASE(test_translate_observations) { - std::stringstream ss(R"""(Column1,Room Type,Monthly Rent,County,State -0,studio,,Mahoning County,OH -1,4br,2152.0,,NV -2,1br,1267.0,Gwinnett County, +BOOST_AUTO_TEST_CASE(test_incorporate_observations) { + std::mt19937 prng; + + std::stringstream ss(R"""( +class School + name ~ string + degree_dist ~ categorical(k=100) + +class Physician + school ~ School + degree ~ stringcat(strings="MD PT NP DO PHD") + specialty ~ stringcat(strings="Family Med:Internal Med:Physical Therapy", delim=":") + # observed_degree ~ maybe_swap(degree) + +class City + name ~ string + state ~ stringcat(strings="AL AK AZ AR CA CO CT DE DC FL GA HI ID IL IN IA KS KY LA ME MD MA MI MN MS MO MT NE NV NH NJ NM NY NC ND OH OK OR PA RI SC SD TN TX UT VT VA WA WV WI WY") + +class Practice + city ~ City + +class Record + physician ~ Physician + location ~ Practice + +observe + physician.specialty as Specialty + physician.school.name as School + physician.degree as Degree + location.city.name as City + location.city.state as State + from Record +)"""); + + PCleanSchema pclean_schema; + BOOST_TEST(read_schema(ss, &pclean_schema)); + + GenDB gendb(&prng, pclean_schema); + + std::stringstream ss2( +R"""(Specialty,School,Degree,City,State +Internal Medicine,Harvard,MD,Somerville,MA +Brain Surgery,UCSF,PhD,San Diego,CA +Dermatology,Duke,MD,Chicago,IL +Internal Medicine,John Hopkins,MD,Washington,DC +Pediatrics,Harvard,MD,Seattle,WA +)"""); + + DataFrame df = DataFrame::from_csv(ss2); + + incorporate_observations(&prng, &gendb, df); + BOOST_TEST(gendb.domain_crps["Practice"].N == 5); + BOOST_TEST(gendb.domain_crps["Physician"].N == 5); +} + +BOOST_AUTO_TEST_CASE(test_incorporate_observations_diagonal) { + std::mt19937 prng; + + std::stringstream ss(R"""( +class City + name ~ string + state ~ stringcat(strings="AL AK AZ AR CA CO CT DE DC FL GA HI ID IL IN IA KS KY LA ME MD MA MI MN MS MO MT NE NV NH NJ NM NY NC ND OH OK OR PA RI SC SD TN TX UT VT VA WA WV WI WY") + +class School + name ~ string + degree_dist ~ categorical(k=100) + city ~ City + +class Physician + school ~ School + degree ~ stringcat(strings="MD PT NP DO PHD") + specialty ~ stringcat(strings="Family Med:Internal Med:Physical Therapy", delim=":") + # observed_degree ~ maybe_swap(degree) + +class Practice + city ~ City + +class Record + physician ~ Physician + location ~ Practice + +observe + physician.specialty as Specialty + physician.school.name as School + physician.school.city.name as SchoolCity + physician.degree as Degree + location.city.name as City + location.city.state as State + from Record +)"""); + + PCleanSchema pclean_schema; + BOOST_TEST(read_schema(ss, &pclean_schema)); + + GenDB gendb(&prng, pclean_schema); + + std::stringstream ss2( +R"""(Specialty,School,SchoolCity,Degree,City,State +Internal Medicine,Harvard,Cambridge,MD,Somerville,MA +Brain Surgery,UCSF,San Francisco,PhD,San Diego,CA +Dermatology,Duke,Durham,MD,Chicago,IL +Internal Medicine,John Hopkins,Baltimore,MD,Washington,DC +Pediatrics,Harvard,Cambridge,MD,Seattle,WA )"""); - DataFrame df = DataFrame::from_csv(ss); - - std::map state_params = {{"strings", "AL AK AZ AR CA CO CT DE DC FL GA HI ID IL IN IA KS KY LA ME MD MA MI MN MS MO MT NE NV NH NJ NM NY NC ND OH OK OR PA RI SC SD TN TX UT VT VA WA WV WI WY"}}; - std::map br_params = {{"strings", "1br 2br 3br 4br studio"}}; - - T_schema schema = { - {"County:name", - T_clean_relation{{"dCounty"}, false, DistributionSpec("bigram")}}, - {"County:state", - T_clean_relation{{"dCounty"}, false, DistributionSpec("stringcat", state_params)}}, - {"Room Type", - T_clean_relation{{"dObs"}, true, DistributionSpec("stringcat", br_params)}}, - {"Monthly Rent", - T_clean_relation{{"dObs"}, true, DistributionSpec("normal")}}, - {"County", - T_noisy_relation{{"dCounty", "dObs"}, true, EmissionSpec("bigram"), "County:name"}}, - {"State", - T_noisy_relation{{"dCounty", "dObs"}, true, EmissionSpec("bigram"), "County:state"}}}; - - std::map> annotated_domains_for_relations; - annotated_domains_for_relations["Room Type"] = {"Obs"}; - annotated_domains_for_relations["Monthly Rent"] = {"Obs"}; - annotated_domains_for_relations["County"] = {"county:County", "Obs"}; - annotated_domains_for_relations["State"] = {"county:County", "Obs"}; - - T_observations obs = translate_observations( - df, schema, annotated_domains_for_relations); - - // Relations not corresponding to columns should be un-observed. - BOOST_TEST(!obs.contains("County:name")); - BOOST_TEST(!obs.contains("County:state")); - - BOOST_TEST(obs["Room Type"].size() == 3); - BOOST_TEST(obs["Monthly Rent"].size() == 2); - BOOST_TEST(obs["County"].size() == 2); - BOOST_TEST(obs["State"].size() == 2); - - BOOST_TEST(std::get<0>(obs["Room Type"][0]).size() == 1); - BOOST_TEST(std::get<1>(obs["Room Type"][0]) == "studio"); - - BOOST_TEST(std::get<0>(obs["Monthly Rent"][0]).size() == 1); - BOOST_TEST(std::get<1>(obs["Monthly Rent"][0]) == "2152.0"); - - BOOST_TEST(std::get<0>(obs["County"][0]).size() == 2); - BOOST_TEST(std::get<1>(obs["County"][0]) == "Mahoning County"); - - BOOST_TEST(std::get<0>(obs["State"][0]).size() == 2); - BOOST_TEST(std::get<1>(obs["State"][0]) == "OH"); + DataFrame df = DataFrame::from_csv(ss2); + + incorporate_observations(&prng, &gendb, df); + BOOST_TEST(gendb.domain_crps["Practice"].N == 5); + // TODO(thomaswc): Figure out why the next BOOST_TEST is failing. + // (.N == 4 instead of the expected 10). + // BOOST_TEST(gendb.domain_crps["City"].N == 10); } BOOST_AUTO_TEST_CASE(test_make_pclean_samples) { std::mt19937 prng; - std::map> annotated_domains_for_relation; std::stringstream ss(R"""( class School @@ -102,20 +157,12 @@ observe PCleanSchema pclean_schema; BOOST_TEST(read_schema(ss, &pclean_schema)); - PCleanSchemaHelper schema_helper(pclean_schema); - T_schema hirm_schema = schema_helper.make_hirm_schema( - &annotated_domains_for_relation); - - HIRM hirm(hirm_schema, &prng); - - // TODO: Re-enable test when it's fixed to sample non-duplicate entities. - // printf("DEBUG: before\n"); - // DataFrame samples = make_pclean_samples( - // 10, &hirm, pclean_schema, annotated_domains_for_relation, &prng); - // printf("DEBUG: after\n"); - // BOOST_TEST(samples.data["Specialty"].size() == 10); - // BOOST_TEST(samples.data["School"].size() == 10); - // BOOST_TEST(samples.data["Degree"].size() == 10); - // BOOST_TEST(samples.data["City"].size() == 10); - // BOOST_TEST(samples.data["State"].size() == 10); + GenDB gendb(&prng, pclean_schema); + + DataFrame samples = make_pclean_samples(10, 0, &gendb, &prng); + BOOST_TEST(samples.data["Specialty"].size() == 10); + BOOST_TEST(samples.data["School"].size() == 10); + BOOST_TEST(samples.data["Degree"].size() == 10); + BOOST_TEST(samples.data["City"].size() == 10); + BOOST_TEST(samples.data["State"].size() == 10); } diff --git a/cxx/pclean/schema_helper.cc b/cxx/pclean/schema_helper.cc deleted file mode 100644 index d3298b9..0000000 --- a/cxx/pclean/schema_helper.cc +++ /dev/null @@ -1,259 +0,0 @@ -#include "pclean/schema_helper.hh" - -#include - -#include "pclean/get_joint_relations.hh" - -PCleanSchemaHelper::PCleanSchemaHelper(const PCleanSchema& s, - bool _only_final_emissions, - bool _record_class_is_clean) - : schema(s), - only_final_emissions(_only_final_emissions), - record_class_is_clean(_record_class_is_clean) { - // Note that the domains cache must be populated before the reference - // indices. - compute_domains_cache(); - compute_reference_indices_cache(); -} - -void PCleanSchemaHelper::compute_domains_cache() { - for (const auto& c : schema.classes) { - if (!domains.contains(c.first)) { - compute_domains_for(c.first); - } - } -} - -void PCleanSchemaHelper::compute_reference_indices_cache() { - for (const auto& c : schema.classes) { - if (!class_reference_indices.contains(c.first)) { - compute_reference_indices_for(c.first); - } - } -} - -void PCleanSchemaHelper::compute_domains_for(const std::string& name) { - std::vector ds; - std::vector annotated_ds; - PCleanClass c = schema.classes[name]; - - for (const auto& v : c.vars) { - if (const ClassVar* cv = std::get_if(&(v.second.spec))) { - if (!domains.contains(cv->class_name)) { - compute_domains_for(cv->class_name); - } - for (const std::string& s : domains[cv->class_name]) { - ds.push_back(s); - } - for (const std::string& s : annotated_domains[cv->class_name]) { - annotated_ds.push_back(v.first + ':' + s); - } - } - } - - // Put the "primary" domain last, so that it survives reordering. - ds.push_back(name); - annotated_ds.push_back(name); - - domains[name] = ds; - annotated_domains[name] = annotated_ds; -} - -void PCleanSchemaHelper::compute_reference_indices_for( - const std::string& name) { - std::vector ds; - int total_offset = 0; - PCleanClass c = schema.classes[name]; - - // Recursively maps the indices of class "name" (and ancestors) in relation - // items to the names and indices (in items) of their parents (reference - // fields). - std::map> ref_indices; - - // Temporarily stores reference fields and indices for class "name"; - std::map class_ref_indices; - for (const auto& v : c.vars) { - if (const ClassVar* cv = std::get_if(&(v.second.spec))) { - if (!class_reference_indices.contains(cv->class_name)) { - compute_reference_indices_for(cv->class_name); - } - // Indices for foreign-key domains are generated by adding an offset - // to their indices in the respective class. - const int offset = total_offset; - total_offset += domains.at(cv->class_name).size(); - class_ref_indices[v.first] = total_offset - 1; - std::map child_class_indices; - if (class_reference_indices.contains(cv->class_name)) { - for (const auto& [ind, ref] : - class_reference_indices.at(cv->class_name)) { - std::map class_ref_indices; - for (const auto& [field_name, ref_ind] : ref) { - child_class_indices[field_name] = ref_ind + offset; - } - ref_indices[ind + offset] = child_class_indices; - } - } - } - } - - // Do not store a `class_reference_indices` entry for classes - // with no reference fields. - if (class_ref_indices.size() > 0) { - ref_indices[total_offset] = class_ref_indices; - class_reference_indices[name] = ref_indices; - } -} - -void PCleanSchemaHelper::make_relations_for_queryfield( - const QueryField& f, const PCleanClass& record_class, T_schema* tschema, - std::map>* - annotated_domains_for_relation) { - // First, find all the vars and classes specified in f.class_path. - std::vector var_names; - std::vector class_names; - PCleanVariable last_var; - PCleanClass last_class = record_class; - class_names.push_back(record_class.name); - for (size_t i = 0; i < f.class_path.size(); ++i) { - const PCleanVariable& v = last_class.vars[f.class_path[i]]; - last_var = v; - var_names.push_back(v.name); - if (i < f.class_path.size() - 1) { - class_names.push_back(std::get(v.spec).class_name); - last_class = schema.classes[class_names.back()]; - } - } - // Remove the last var_name because it isn't used in making the path_prefix. - var_names.pop_back(); - - // Get the base relation from the last class and variable name. - std::string base_relation_name = class_names.back() + ":" + last_var.name; - - // Handle queries of the record class specially. - if (f.class_path.size() == 1) { - if (record_class_is_clean) { - // Just rename the existing clean relation and set it to be observed. - T_clean_relation cr = - std::get(tschema->at(base_relation_name)); - cr.is_observed = true; - (*tschema)[f.name] = cr; - tschema->erase(base_relation_name); - (*annotated_domains_for_relation)[f.name] = - annotated_domains[record_class.name]; - } else { - T_noisy_relation tnr = - get_emission_relation(std::get(last_var.spec), - domains[record_class.name], base_relation_name); - tnr.is_observed = true; - (*tschema)[f.name] = tnr; - (*annotated_domains_for_relation)[f.name] = - annotated_domains[record_class.name]; - // If the record class is the only class in the schema, there will be - // no entries in `relation_reference_indices`. - if (class_reference_indices.contains(record_class.name)) { - relation_reference_indices[f.name] = - class_reference_indices.at(record_class.name); - } - } - return; - } - - // Handle only_final_emissions == true. - if (only_final_emissions) { - std::vector noisy_domains = domains[class_names.back()]; - std::vector adfr = annotated_domains[class_names.back()]; - for (int i = class_names.size() - 2; i >= 0; --i) { - noisy_domains.push_back(class_names[i]); - for (size_t j = 0; j < adfr.size(); ++j) { - adfr[j] = var_names[i] + ":" + adfr[j]; - } - adfr.push_back(class_names[i]); - relation_reference_indices[f.name][noisy_domains.size() - 1] - [var_names[i]] = noisy_domains.size() - 2; - } - T_noisy_relation tnr = get_emission_relation( - std::get(last_var.spec), noisy_domains, base_relation_name); - tnr.is_observed = true; - (*tschema)[f.name] = tnr; - (*annotated_domains_for_relation)[f.name] = adfr; - // If the record class is the only class in the schema, there will be - // no entries in `relation_reference_indices`. - if (relation_reference_indices.contains(base_relation_name)) { - relation_reference_indices[f.name] = - relation_reference_indices.at(base_relation_name); - } - return; - } - - // Handle only_final_emissions == false. - std::string& previous_relation = base_relation_name; - std::vector current_domains = domains[class_names.back()]; - std::vector adfr = annotated_domains[class_names.back()]; - std::map> ref_indices; - for (int i = f.class_path.size() - 2; i >= 0; --i) { - current_domains.push_back(class_names[i]); - for (size_t j = 0; j < adfr.size(); ++j) { - adfr[j] = var_names[i] + ":" + adfr[j]; - } - adfr.push_back(class_names[i]); - ref_indices[current_domains.size() - 1][var_names[i]] = - current_domains.size() - 2; - T_noisy_relation tnr = get_emission_relation( - std::get(last_var.spec), current_domains, previous_relation); - std::string rel_name; - if (i == 0) { - rel_name = f.name; - tnr.is_observed = true; - } else { - // Intermediate emissions have a name of the form - // "[Observing Class]::[QueryFieldName]" - rel_name = class_names[i] + "::" + f.name; - tnr.is_observed = false; - } - (*tschema)[rel_name] = tnr; - // Since noisy relations have the leftmost domains in common with their base - // relations, they share the reference indices with their base relations as - // well. - if (relation_reference_indices.contains(previous_relation)) { - relation_reference_indices[rel_name] = - relation_reference_indices.at(previous_relation); - } - relation_reference_indices[rel_name].merge(ref_indices); - previous_relation = rel_name; - (*annotated_domains_for_relation)[rel_name] = adfr; - } -} - -T_schema PCleanSchemaHelper::make_hirm_schema( - std::map>* - annotated_domains_for_relation) { - T_schema tschema; - - // For every scalar variable, make a clean relation with the name - // "[ClassName]:[VariableName]". - for (const auto& c : schema.classes) { - for (const auto& v : c.second.vars) { - std::string rel_name = c.first + ':' + v.first; - if (const ScalarVar* dv = std::get_if(&(v.second.spec))) { - tschema[rel_name] = get_distribution_relation(*dv, domains[c.first]); - (*annotated_domains_for_relation)[rel_name] = - annotated_domains[c.first]; - if (class_reference_indices.contains(c.first)) { - relation_reference_indices[rel_name] = - class_reference_indices.at(c.first); - } - } - } - } - - // For every query field, make one or more relations by walking up - // the class_path. At least one of those relations will have name equal - // to the name of the QueryField. - const PCleanClass record_class = schema.classes[schema.query.record_class]; - for (const auto& [unused_name, f] : schema.query.fields) { - make_relations_for_queryfield(f, record_class, &tschema, - annotated_domains_for_relation); - } - - return tschema; -} diff --git a/cxx/pclean/schema_helper.hh b/cxx/pclean/schema_helper.hh deleted file mode 100644 index e750d8e..0000000 --- a/cxx/pclean/schema_helper.hh +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2024 -// See LICENSE.txt - -#pragma once - -#include -#include -#include - -#include "irm.hh" -#include "pclean/schema.hh" - -// A class for quickly computing various properties of the schema. -class PCleanSchemaHelper { - public: - PCleanSchemaHelper(const PCleanSchema& s, bool _only_final_emissions = false, - bool _record_class_is_clean = true); - - // Translate the PCleanSchema into an HIRM T_schema. - // Also, fill annotated_domains_for_relation[r] with the vector of - // annotated domains for the relation r. - T_schema make_hirm_schema(std::map>* - annotated_domains_for_relation); - - // The rest of these methods are conceptually private, but actually - // public for testing. - - void compute_domains_cache(); - - void compute_domains_for(const std::string& name); - - void compute_reference_indices_cache(); - - void compute_reference_indices_for(const std::string& name); - - void make_relations_for_queryfield( - const QueryField& f, const PCleanClass& c, T_schema* schema, - std::map>* - annotated_domains_for_relation); - - PCleanSchema schema; - bool only_final_emissions; - bool record_class_is_clean; - std::map> domains; - std::map> annotated_domains; - - // Map keys are relation name, item index of a class, and reference field - // name. The values in the inner map are the item index of the reference - // class. (See tests for more intuition.) - std::map>> - relation_reference_indices; - - // Map keys are class name, item index of a class, and reference field - // name. The values in the inner map are the item index of the reference - // class. (See tests for more intuition.) - std::map>> - class_reference_indices; -}; diff --git a/cxx/pclean/schema_helper_test.cc b/cxx/pclean/schema_helper_test.cc deleted file mode 100644 index 3b9a0d5..0000000 --- a/cxx/pclean/schema_helper_test.cc +++ /dev/null @@ -1,473 +0,0 @@ -#define BOOST_TEST_MODULE test pclean_schema - -#include "pclean/schema_helper.hh" - -#include -#include - -#include "pclean/io.hh" -namespace tt = boost::test_tools; - -struct SchemaTestFixture { - SchemaTestFixture() { - std::stringstream ss(R"""( -class School - name ~ string - degree_dist ~ categorical(k=100) - -class Physician - school ~ School - degree ~ stringcat(strings="MD PT NP DO PHD") - specialty ~ stringcat(strings="Family Med:Internal Med:Physical Therapy", delim=":") - # observed_degree ~ maybe_swap(degree) - -class City - name ~ string - state ~ stringcat(strings="AL AK AZ AR CA CO CT DE DC FL GA HI ID IL IN IA KS KY LA ME MD MA MI MN MS MO MT NE NV NH NJ NM NY NC ND OH OK OR PA RI SC SD TN TX UT VT VA WA WV WI WY") - -class Practice - city ~ City - -class Record - physician ~ Physician - location ~ Practice - -observe - physician.specialty as Specialty - physician.school.name as School - physician.degree as Degree - location.city.name as City - location.city.state as State - from Record -)"""); - [[maybe_unused]] bool ok = read_schema(ss, &schema); - assert(ok); - } - - ~SchemaTestFixture() {} - - PCleanSchema schema; -}; - -BOOST_FIXTURE_TEST_SUITE(schema_test_suite, SchemaTestFixture) - -BOOST_AUTO_TEST_CASE(test_domains_cache) { - PCleanSchemaHelper schema_helper(schema); - - std::vector expected_domains = {"School"}; - std::vector expected_annotated_domains = {"School"}; - BOOST_TEST(schema_helper.domains["School"] == expected_domains); - BOOST_TEST(schema_helper.annotated_domains["School"] == - expected_annotated_domains); - - expected_domains = {"School", "Physician"}; - expected_annotated_domains = {"school:School", "Physician"}; - BOOST_TEST(schema_helper.domains["Physician"] == expected_domains); - BOOST_TEST(schema_helper.annotated_domains["Physician"] == - expected_annotated_domains); - - expected_domains = {"City"}; - expected_annotated_domains = {"City"}; - BOOST_TEST(schema_helper.domains["City"] == expected_domains); - BOOST_TEST(schema_helper.annotated_domains["City"] == - expected_annotated_domains); - - expected_domains = {"City", "Practice"}; - expected_annotated_domains = {"city:City", "Practice"}; - BOOST_TEST(schema_helper.domains["Practice"] == expected_domains); - BOOST_TEST(schema_helper.annotated_domains["Practice"] == - expected_annotated_domains); - - expected_domains = {"City", "Practice", "School", "Physician", "Record"}; - expected_annotated_domains = {"location:city:City", "location:Practice", - "physician:school:School", - "physician:Physician", "Record"}; - BOOST_TEST(schema_helper.domains["Record"] == expected_domains, - tt::per_element()); - BOOST_TEST( - schema_helper.annotated_domains["Record"] == expected_annotated_domains, - tt::per_element()); - - auto& ref_indices = schema_helper.class_reference_indices; - - // The Practice, Physician, and Record classes have reference fields, so they - // should be included in the reference field index map. - BOOST_TEST(ref_indices.size() == 3); - - // For Physician and Practice, index 1 corresponds to the class itself, and - // index 0 corresponds to the reference class. - BOOST_TEST(ref_indices.at("Physician").at(1).at("school") == 0); - BOOST_TEST(ref_indices.at("Practice").at(1).at("city") == 0); - - // For Record, index 4 corresponds to the class itself, which points to - // physician (index 3) and location (index 1). - BOOST_TEST(ref_indices.at("Record").at(4).at("physician") == 3); - BOOST_TEST(ref_indices.at("Record").at(4).at("location") == 1); - BOOST_TEST(ref_indices.at("Record").at(3).at("school") == 2); - BOOST_TEST(ref_indices.at("Record").at(1).at("city") == 0); -} - -BOOST_AUTO_TEST_CASE(test_domains_and_reference_cache_two_paths_same_source) { - std::stringstream ss(R"""( -class City - name ~ string - -class Person - birth_city ~ City - home_city ~ City -)"""); - PCleanSchema schema; - [[maybe_unused]] bool ok = read_schema(ss, &schema); - assert(ok); - PCleanSchemaHelper schema_helper(schema); - - std::vector expected_domains = {"City", "City", "Person"}; - std::vector expected_annotated_domains = { - "birth_city:City", "home_city:City", "Person"}; - BOOST_TEST(schema_helper.domains["Person"] == expected_domains, - tt::per_element()); - BOOST_TEST( - schema_helper.annotated_domains["Person"] == expected_annotated_domains, - tt::per_element()); - - auto& ref_indices = schema_helper.class_reference_indices; - - // Only the Person field has reference fields. - BOOST_TEST(ref_indices.size() == 1); - BOOST_TEST(ref_indices.at("Person").at(2).at("birth_city") == 0); - BOOST_TEST(ref_indices.at("Person").at(2).at("home_city") == 1); -} - -BOOST_AUTO_TEST_CASE(test_domains_and_reference_cache_diamond) { - std::stringstream ss(R"""( -class City - name ~ string - -class School - location ~ City - -class Practice - location ~ City - -class Physician - practice ~ Practice - school ~ School -)"""); - PCleanSchema schema; - [[maybe_unused]] bool ok = read_schema(ss, &schema); - assert(ok); - PCleanSchemaHelper schema_helper(schema); - - std::vector expected_domains = {"City", "Practice", "City", - "School", "Physician"}; - std::vector expected_annotated_domains = { - "practice:location:City", "practice:Practice", "school:location:City", - "school:School", "Physician"}; - BOOST_TEST(schema_helper.domains["Physician"] == expected_domains, - tt::per_element()); - BOOST_TEST(schema_helper.annotated_domains["Physician"] == - expected_annotated_domains, - tt::per_element()); - - auto& ref_indices = schema_helper.class_reference_indices; - - BOOST_TEST(ref_indices.size() == 3); - - // Physician (index 4) has a reference field "practice", which appears - // at index 1. Practice has a reference field "location", which appears - // at index 0. - BOOST_TEST(ref_indices.at("Physician").at(4).at("practice") == 1); - BOOST_TEST(ref_indices.at("Physician").at(1).at("location") == 0); - - // Physician (index 4) has a reference field "school", which appears - // at index 3. School has a reference field "location", which appears - // at index 2. - BOOST_TEST(ref_indices.at("Physician").at(4).at("school") == 3); - BOOST_TEST(ref_indices.at("Physician").at(3).at("location") == 2); - - BOOST_TEST(ref_indices.at("Practice").at(1).at("location") == 0); - BOOST_TEST(ref_indices.at("School").at(1).at("location") == 0); -} - -BOOST_AUTO_TEST_CASE(test_make_relations_for_queryfield) { - PCleanSchemaHelper schema_helper(schema); - T_schema tschema; - - PCleanClass query_class = schema.classes[schema.query.record_class]; - std::map> - annotated_domains_for_relation; - schema_helper.make_relations_for_queryfield(schema.query.fields["School"], - query_class, &tschema, - &annotated_domains_for_relation); - - BOOST_TEST(tschema.size() == 2); - BOOST_TEST(tschema.contains("School")); - BOOST_TEST(tschema.contains("Physician::School")); - BOOST_TEST(std::get(tschema["School"]).is_observed); - BOOST_TEST( - !std::get(tschema["Physician::School"]).is_observed); - - std::vector expected_adfr = {"physician:school:School", - "physician:Physician", "Record"}; - BOOST_TEST(annotated_domains_for_relation["School"] == expected_adfr, - tt::per_element()); -} - -BOOST_AUTO_TEST_CASE(test_make_relations_for_queryfield_only_final_emissions) { - PCleanSchemaHelper schema_helper(schema, true); - T_schema tschema; - - PCleanClass query_class = schema.classes[schema.query.record_class]; - std::map> - annotated_domains_for_relation; - schema_helper.make_relations_for_queryfield(schema.query.fields["School"], - query_class, &tschema, - &annotated_domains_for_relation); - - BOOST_TEST(tschema.size() == 1); - BOOST_TEST(tschema.contains("School")); -} - -BOOST_AUTO_TEST_CASE(test_make_hirm_schmea) { - PCleanSchemaHelper schema_helper(schema); - std::map> - annotated_domains_for_relation; - T_schema tschema = - schema_helper.make_hirm_schema(&annotated_domains_for_relation); - - BOOST_TEST(tschema.contains("School:name")); - T_clean_relation cr = std::get(tschema["School:name"]); - BOOST_TEST(!cr.is_observed); - BOOST_TEST((cr.distribution_spec.distribution == DistributionEnum::bigram)); - std::vector expected_domains = {"School"}; - BOOST_TEST(cr.domains == expected_domains); - - BOOST_TEST(tschema.contains("School:degree_dist")); - T_clean_relation cr2 = - std::get(tschema["School:degree_dist"]); - BOOST_TEST( - (cr2.distribution_spec.distribution == DistributionEnum::categorical)); - BOOST_TEST(cr2.distribution_spec.distribution_args.contains("k")); - BOOST_TEST(cr2.domains == expected_domains); - - BOOST_TEST(tschema.contains("Physician:degree")); - T_clean_relation cr3 = - std::get(tschema["Physician:degree"]); - BOOST_TEST( - (cr3.distribution_spec.distribution == DistributionEnum::stringcat)); - std::vector expected_domains2 = {"School", "Physician"}; - BOOST_TEST(cr3.domains == expected_domains2); - - BOOST_TEST(tschema.contains("Physician:specialty")); - - BOOST_TEST(tschema.contains("City:name")); - T_clean_relation cr4 = std::get(tschema["City:name"]); - std::vector expected_domains3 = {"City"}; - BOOST_TEST(cr4.domains == expected_domains3); - - BOOST_TEST(tschema.contains("City:state")); - - BOOST_TEST(tschema.contains("Specialty")); - T_noisy_relation nr1 = std::get(tschema["Specialty"]); - BOOST_TEST(nr1.is_observed); - BOOST_TEST((nr1.emission_spec.emission == EmissionEnum::bigram_string)); - expected_domains = {"School", "Physician", "Record"}; - BOOST_TEST(nr1.domains == expected_domains, tt::per_element()); - - BOOST_TEST(tschema.contains("School")); - T_noisy_relation nr2 = std::get(tschema["School"]); - BOOST_TEST(nr2.is_observed); - BOOST_TEST((nr2.emission_spec.emission == EmissionEnum::bigram_string)); - expected_domains = {"School", "Physician", "Record"}; - BOOST_TEST(nr2.domains == expected_domains, tt::per_element()); - - BOOST_TEST(tschema.contains("Degree")); - T_noisy_relation nr3 = std::get(tschema["Degree"]); - BOOST_TEST(nr3.is_observed); - BOOST_TEST((nr3.emission_spec.emission == EmissionEnum::bigram_string)); - expected_domains = {"School", "Physician", "Record"}; - BOOST_TEST(nr3.domains == expected_domains, tt::per_element()); - - BOOST_TEST(tschema.contains("City")); - T_noisy_relation nr4 = std::get(tschema["City"]); - BOOST_TEST(nr4.is_observed); - BOOST_TEST((nr4.emission_spec.emission == EmissionEnum::bigram_string)); - expected_domains = {"City", "Practice", "Record"}; - BOOST_TEST(nr4.domains == expected_domains, tt::per_element()); - - BOOST_TEST(tschema.contains("State")); - T_noisy_relation nr5 = std::get(tschema["State"]); - BOOST_TEST(nr5.is_observed); - BOOST_TEST((nr5.emission_spec.emission == EmissionEnum::bigram_string)); - expected_domains = {"City", "Practice", "Record"}; - BOOST_TEST(nr5.domains == expected_domains, tt::per_element()); - - BOOST_TEST(tschema.contains("Physician::School")); - T_noisy_relation nr6 = - std::get(tschema["Physician::School"]); - BOOST_TEST(!nr6.is_observed); - expected_domains = {"School", "Physician"}; - BOOST_TEST(nr6.domains == expected_domains, tt::per_element()); - - BOOST_TEST(tschema.contains("Practice::City")); - T_noisy_relation nr7 = std::get(tschema["Practice::City"]); - BOOST_TEST(!nr7.is_observed); - expected_domains = {"City", "Practice"}; - BOOST_TEST(nr7.domains == expected_domains, tt::per_element()); - - BOOST_TEST(tschema.contains("Practice::State")); - T_noisy_relation nr8 = std::get(tschema["Practice::State"]); - BOOST_TEST(!nr8.is_observed); - expected_domains = {"City", "Practice"}; - BOOST_TEST(nr8.domains == expected_domains, tt::per_element()); - - auto& ref_indices = schema_helper.relation_reference_indices; - - // Practice (index 1) has a reference field "city", which appears - // at index 0. - BOOST_TEST(ref_indices.at("Practice::State").at(1).at("city") == 0); - - // Record (index 2) has a reference field "location", which appears - // at index 1 (and refers to Practice). Practice has a reference field - // "city", which appears at index 0. - BOOST_TEST(ref_indices.at("State").at(2).at("location") == 1); - BOOST_TEST(ref_indices.at("State").at(1).at("city") == 0); -} - -BOOST_AUTO_TEST_CASE(test_make_hirm_schema_only_final_emissions) { - PCleanSchemaHelper schema_helper(schema, true); - std::map> - annotated_domains_for_relation; - T_schema tschema = - schema_helper.make_hirm_schema(&annotated_domains_for_relation); - - BOOST_TEST(tschema.contains("School:name")); - T_clean_relation cr = std::get(tschema["School:name"]); - BOOST_TEST(!cr.is_observed); - BOOST_TEST((cr.distribution_spec.distribution == DistributionEnum::bigram)); - std::vector expected_domains = {"School"}; - BOOST_TEST(cr.domains == expected_domains); - - BOOST_TEST(tschema.contains("School:degree_dist")); - T_clean_relation cr2 = - std::get(tschema["School:degree_dist"]); - BOOST_TEST( - (cr2.distribution_spec.distribution == DistributionEnum::categorical)); - BOOST_TEST(cr2.distribution_spec.distribution_args.contains("k")); - BOOST_TEST(cr2.domains == expected_domains); - - BOOST_TEST(tschema.contains("Physician:degree")); - T_clean_relation cr3 = - std::get(tschema["Physician:degree"]); - BOOST_TEST( - (cr3.distribution_spec.distribution == DistributionEnum::stringcat)); - std::vector expected_domains2 = {"School", "Physician"}; - BOOST_TEST(cr3.domains == expected_domains2); - - BOOST_TEST(tschema.contains("Physician:specialty")); - - BOOST_TEST(tschema.contains("City:name")); - T_clean_relation cr4 = std::get(tschema["City:name"]); - std::vector expected_domains3 = {"City"}; - BOOST_TEST(cr4.domains == expected_domains3); - - BOOST_TEST(tschema.contains("City:state")); - - BOOST_TEST(tschema.contains("Specialty")); - T_noisy_relation nr1 = std::get(tschema["Specialty"]); - BOOST_TEST(nr1.is_observed); - BOOST_TEST((nr1.emission_spec.emission == EmissionEnum::bigram_string)); - expected_domains = {"School", "Physician", "Record"}; - BOOST_TEST(nr1.domains == expected_domains, tt::per_element()); - - BOOST_TEST(tschema.contains("School")); - T_noisy_relation nr2 = std::get(tschema["School"]); - BOOST_TEST(nr2.is_observed); - BOOST_TEST((nr2.emission_spec.emission == EmissionEnum::bigram_string)); - expected_domains = {"School", "Physician", "Record"}; - BOOST_TEST(nr2.domains == expected_domains, tt::per_element()); - - BOOST_TEST(tschema.contains("Degree")); - T_noisy_relation nr3 = std::get(tschema["Degree"]); - BOOST_TEST(nr3.is_observed); - BOOST_TEST((nr3.emission_spec.emission == EmissionEnum::bigram_string)); - expected_domains = {"School", "Physician", "Record"}; - BOOST_TEST(nr3.domains == expected_domains, tt::per_element()); - - BOOST_TEST(tschema.contains("City")); - T_noisy_relation nr4 = std::get(tschema["City"]); - BOOST_TEST(nr4.is_observed); - BOOST_TEST((nr4.emission_spec.emission == EmissionEnum::bigram_string)); - expected_domains = {"City", "Practice", "Record"}; - BOOST_TEST(nr4.domains == expected_domains, tt::per_element()); - - BOOST_TEST(tschema.contains("State")); - T_noisy_relation nr5 = std::get(tschema["State"]); - BOOST_TEST(nr5.is_observed); - BOOST_TEST((nr5.emission_spec.emission == EmissionEnum::bigram_string)); - expected_domains = {"City", "Practice", "Record"}; - BOOST_TEST(nr5.domains == expected_domains, tt::per_element()); - - auto& ref_indices = schema_helper.relation_reference_indices; - BOOST_TEST(ref_indices.at("State").at(2).at("location") == 1); - BOOST_TEST(ref_indices.at("State").at(1).at("city") == 0); -} - -BOOST_AUTO_TEST_CASE(test_record_class_is_clean) { - std::stringstream ss2(R"""( -class Record - rent ~ real - -observe - rent as "Rent" - from Record -)"""); - PCleanSchema schema2; - [[maybe_unused]] bool ok = read_schema(ss2, &schema2); - assert(ok); - - PCleanSchemaHelper schema_helper(schema2, false, true); - std::map> - annotated_domains_for_relation; - T_schema tschema = - schema_helper.make_hirm_schema(&annotated_domains_for_relation); - - BOOST_TEST(!tschema.contains("Record:rent")); - BOOST_TEST(tschema.contains("Rent")); - - T_clean_relation cr = std::get(tschema["Rent"]); - BOOST_TEST(cr.is_observed); -} - -BOOST_AUTO_TEST_CASE(test_record_class_is_dirty) { - std::stringstream ss2(R"""( -class Record - rent ~ real - -observe - rent as "Rent" - from Record -)"""); - PCleanSchema schema2; - [[maybe_unused]] bool ok = read_schema(ss2, &schema2); - assert(ok); - - PCleanSchemaHelper schema_helper(schema2, false, false); - std::map> - annotated_domains_for_relation; - T_schema tschema = - schema_helper.make_hirm_schema(&annotated_domains_for_relation); - - BOOST_TEST(tschema.contains("Record:rent")); - BOOST_TEST(tschema.contains("Rent")); - - T_clean_relation cr = std::get(tschema["Record:rent"]); - BOOST_TEST(!cr.is_observed); - T_noisy_relation nr = std::get(tschema["Rent"]); - BOOST_TEST(nr.is_observed); - - std::vector expected_adfr = {"Record"}; - BOOST_TEST(annotated_domains_for_relation["Rent"] == expected_adfr); -} - -BOOST_AUTO_TEST_SUITE_END()