From 142e278cb4e28e2f847a16d62bf3cae1dbadf413 Mon Sep 17 00:00:00 2001 From: DPSynth Team Date: Mon, 17 Aug 2026 09:38:53 -0700 Subject: [PATCH] Introduces the dpsynth.relational submodule skeletons, BUILD rules, and interface contracts for differentially private multi-table relational synthesis. Key additions: - Wrote third_party/py/dpsynth/relational/README.md with short announcement about active development and API. - Created third_party/py/dpsynth/relational/BUILD with pytype_strict_library rules and test targets. - Defined the ForeignKeyRelation dataclass in domain.py with group capacity bounds and cascading sensitivity documentation. - Established strict function and method signatures for topological DAG sorting, schema parsing, pure relational transformations, and the MultiTableConfig / MultiTableMechanism abstractions matching the latest dpsynth architecture. - Added initial unit test suites across domain_test.py, transformations_test.py, and synthesizer_test.py. - Exported public symbols in third_party/py/dpsynth/relational/__init__.py. PiperOrigin-RevId: 966020664 --- dpsynth/relational/README.md | 65 ++++++++ dpsynth/relational/__init__.py | 35 +++++ dpsynth/relational/domain.py | 119 +++++++++++++++ dpsynth/relational/synthesizer.py | 134 +++++++++++++++++ dpsynth/relational/transformations.py | 184 +++++++++++++++++++++++ tests/relational/domain_test.py | 51 +++++++ tests/relational/synthesizer_test.py | 34 +++++ tests/relational/transformations_test.py | 37 +++++ 8 files changed, 659 insertions(+) create mode 100644 dpsynth/relational/README.md create mode 100644 dpsynth/relational/__init__.py create mode 100644 dpsynth/relational/domain.py create mode 100644 dpsynth/relational/synthesizer.py create mode 100644 dpsynth/relational/transformations.py create mode 100644 tests/relational/domain_test.py create mode 100644 tests/relational/synthesizer_test.py create mode 100644 tests/relational/transformations_test.py diff --git a/dpsynth/relational/README.md b/dpsynth/relational/README.md new file mode 100644 index 0000000..6484417 --- /dev/null +++ b/dpsynth/relational/README.md @@ -0,0 +1,65 @@ +# `dpsynth.relational` (Multi-Table Relational DP Synthesis) + +> [!WARNING] +> **UNDER ACTIVE DEVELOPMENT / EXPERIMENTAL**: This module is in an early +> experimental stage and is under active development. The APIs, internal +> interfaces, and algorithms are subject to breaking changes. It is not yet +> recommended for production use. + +--- + +## Overview + +The `dpsynth.relational` package extends DP Synth to support **hierarchical +multi-table relational databases** under differential privacy. + +In relational databases (e.g. `Household -> Person -> Activity`), privacy is +protected at the **root parent record** (e.g. the household), while valid +foreign-key linkages and cross-table statistical correlations are preserved +across child tables without generating orphaned records or requiring +intractable flat Cartesian joins. + +### Core Approach + +- **Cascading Relational Synthesis**: Synthesizes databases table-by-table + down foreign-key hierarchy trees, ensuring root-entity differential privacy + without materializing expensive Cartesian joins. +- **Permutation Modeling**: Captures rich parent-child and sibling-to-sibling + correlations through slot permutation and exchangeability, inspired by + PrivPetal ([Cai et al., 2025](https://arxiv.org/abs/2503.22970)). +- **Built on `dpsynth` & `mbi` Core**: Reuses existing discrete mechanisms (e.g. + AIM, MST) and the `mbi` (Private-PGM) graphical model engine under the hood. +- **Unified Mechanism API**: Integrates directly with the `MechanismConfig` / + `CalibratedMechanism` paradigm used throughout `dpsynth`. + +--- + +## Planned API + +```python +import dpsynth +from dpsynth.relational import ForeignKeyRelation, MultiTableConfig + +# Define schemas and foreign key relations +foreign_keys = [ + ForeignKeyRelation( + parent_table="households", + parent_primary_key="household_id", + child_table="persons", + child_foreign_key="household_id", + max_children_per_parent=5, + ), +] + +# Configure relational synthesizer +config = MultiTableConfig( + domains=table_domains, + foreign_keys=foreign_keys, + discrete_mechanism=dpsynth.discrete_mechanisms.AIMConfig(), +) + +# Calibrate and run +calibrated = config.calibrate(epsilon=1.0, delta=1e-5) +result = calibrated(rng, {"households": df_h, "persons": df_p}) +synth_tables = result.synthetic_tables +``` diff --git a/dpsynth/relational/__init__.py b/dpsynth/relational/__init__.py new file mode 100644 index 0000000..e692cbe --- /dev/null +++ b/dpsynth/relational/__init__.py @@ -0,0 +1,35 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Public API for multi-table relational differential privacy synthesis.""" + +# pylint: disable=g-importing-member + +from dpsynth.relational.domain import ForeignKeyRelation +from dpsynth.relational.domain import from_dict +from dpsynth.relational.domain import from_yaml_file +from dpsynth.relational.domain import topological_sort_hierarchy +from dpsynth.relational.synthesizer import MultiDataGenerationResult +from dpsynth.relational.synthesizer import MultiTableConfig +from dpsynth.relational.synthesizer import MultiTableMechanism + +__all__ = [ + 'ForeignKeyRelation', + 'MultiDataGenerationResult', + 'MultiTableConfig', + 'MultiTableMechanism', + 'from_dict', + 'from_yaml_file', + 'topological_sort_hierarchy', +] diff --git a/dpsynth/relational/domain.py b/dpsynth/relational/domain.py new file mode 100644 index 0000000..edd4cb4 --- /dev/null +++ b/dpsynth/relational/domain.py @@ -0,0 +1,119 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Domain representations, schema definitions, and DAG validators for relational data.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +import dataclasses +from typing import Any + +from dpsynth import domain +from etils import epath +import yaml + +PathType = epath.PathLike + +# pylint: disable=unused-import +_YAML_UNUSED = yaml +# pylint: enable=unused-import + + +@dataclasses.dataclass(frozen=True) +class ForeignKeyRelation: + """Defines a directed foreign key relationship between parent and child tables. + + Attributes: + parent_table: Name of the parent table (e.g. 'households'). + parent_primary_key: Name of the parent primary key column (e.g. + 'household_id'). + child_table: Name of the child table (e.g. 'persons'). + child_foreign_key: Name of the child foreign key column referencing parent. + max_children_per_parent: Maximum number of children associated with a single + parent record (group size capacity bound s). Must be >= 1. Determines the + wide MRF generation slot count (s) and directly scales cascading DP + sensitivity (Delta_k = prod s_ancestors) for downstream child tables. + """ + + parent_table: str + parent_primary_key: str + child_table: str + child_foreign_key: str + max_children_per_parent: int + + def __post_init__(self): + if self.max_children_per_parent < 1: + raise ValueError( + 'max_children_per_parent must be >= 1, got' + f' {self.max_children_per_parent}.' + ) + + +def topological_sort_hierarchy( + tables: Sequence[str], + foreign_keys: Sequence[ForeignKeyRelation], +) -> list[tuple[int, str, ForeignKeyRelation | None]]: + """Validates DAG tree structure and computes topological synthesis levels. + + Args: + tables: Sequence of all table names in the database. + foreign_keys: Sequence of foreign key relationships between tables. + + Returns: + An ordered list of (depth, table_name, foreign_key_relation) tuples, where + depth is 0 for root tables (foreign_key_relation is None) and depth >= 1 for + child tables (foreign_key_relation links the table to its immediate parent). + + Raises: + ValueError: If foreign keys contain cycles, missing tables, or if a child + table references more than one parent table (in-degree > 1). + """ + del tables, foreign_keys + raise NotImplementedError( + 'topological_sort_hierarchy is not yet implemented.' + ) + + +def from_dict( + config: Mapping[str, Any], +) -> tuple[dict[str, domain.Schema], list[ForeignKeyRelation]]: + """Parses multi-table schema and foreign keys from a dictionary. + + Args: + config: Dictionary with 'tables' and optional 'foreign_keys' blocks. + + Returns: + A tuple of (table_domains, foreign_keys). + + Raises: + ValueError: If configuration format or attribute specifications are invalid. + """ + del config + raise NotImplementedError('from_dict is not yet implemented.') + + +def from_yaml_file( + filepath: str | PathType, +) -> tuple[dict[str, domain.Schema], list[ForeignKeyRelation]]: + """Reads multi-table schema and foreign keys from a YAML file. + + Args: + filepath: Path to the YAML schema file. + + Returns: + A tuple of (table_domains, foreign_keys). + """ + del filepath + raise NotImplementedError('from_yaml_file is not yet implemented.') diff --git a/dpsynth/relational/synthesizer.py b/dpsynth/relational/synthesizer.py new file mode 100644 index 0000000..fded2b4 --- /dev/null +++ b/dpsynth/relational/synthesizer.py @@ -0,0 +1,134 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Multi-table relational differential privacy synthesizer.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +import dataclasses +from typing import Any, Literal + +from absl import logging +import dp_accounting +from dpsynth import api +from dpsynth import discrete_mechanisms +from dpsynth import domain +from dpsynth.relational import domain as rel_domain +import numpy as np +import pandas as pd + +# pylint: disable=unused-import +_LOGGING_UNUSED = logging +# pylint: enable=unused-import + + +@dataclasses.dataclass(frozen=True) +class MultiDataGenerationResult: + """Results of multi-table relational DP synthetic data generation. + + Attributes: + synthetic_tables: Mapping from table names to synthetic DataFrames. + discrete_mechanism_results: Mapping from link/table names to mechanism + diagnostics. + """ + + synthetic_tables: Mapping[str, pd.DataFrame] + discrete_mechanism_results: Mapping[str, Any] = dataclasses.field( + default_factory=dict + ) + + +@dataclasses.dataclass +class MultiTableMechanism(api.CalibratedMechanism): + """Calibrated, runnable multi-table relational differential privacy mechanism. + + Attributes: + domains: Mapping from table name to per-column attribute specifications. + foreign_keys: Sequence of foreign key relationships defining the hierarchy. + calibrated_discrete_mechanisms: Mapping from link names to calibrated + discrete mechanisms. + calibrated_initializers: Mapping from table and column to calibrated + initializers. + total_count_sigma: Sigma for the root table total-count mechanism. + num_permutation_slots: Permutation exploration slot count (o), default 2. + exploration_strategy: Exploration strategy ('empty_token' or 'size_sliced'). + max_records_per_user: Assumed upper bound on records a single root user + contributes. + + Note: For simplicity, user-defined contraints are not supported yet. + """ + + domains: Mapping[str, domain.Schema] + foreign_keys: Sequence[rel_domain.ForeignKeyRelation] + calibrated_discrete_mechanisms: Mapping[str, api.CalibratedMechanism] + calibrated_initializers: Mapping[str, Mapping[str, api.CalibratedMechanism]] + total_count_sigma: float = dataclasses.field(repr=False) + num_permutation_slots: int = 2 + exploration_strategy: Literal['empty_token', 'size_sliced'] = 'empty_token' + max_records_per_user: int = 1 + + @property + def dp_event(self) -> dp_accounting.DpEvent: + """Returns the composed DpEvent for all sub-mechanisms.""" + raise NotImplementedError('dp_event is not yet implemented.') + + def __call__( + self, + rng: np.random.Generator, + data: Mapping[str, pd.DataFrame], + ) -> MultiDataGenerationResult: + """Generates synthetic multi-table relational data.""" + del rng, data + raise NotImplementedError( + 'MultiTableMechanism.__call__ is not yet implemented.' + ) + + +@dataclasses.dataclass +class MultiTableConfig(api.MechanismConfig): + """Configuration recipe for multi-table relational differential privacy synthesis. + + Attributes: + domains: Mapping from table name to per-column attribute domain + specifications. + foreign_keys: Sequence of foreign key relationships defining the hierarchy. + discrete_mechanism: Discrete mechanism config (e.g. AIM, MST) for relational + links. + numerical_bins: Number of bins for numerical attribute discretization. + init_budget_fraction: Fraction of total zCDP budget allocated to Phase 1. + num_permutation_slots: Permutation exploration slot count (o), default 2. + exploration_strategy: Exploration strategy ('empty_token' or 'size_sliced'). + """ + + domains: Mapping[str, domain.Schema] + foreign_keys: Sequence[rel_domain.ForeignKeyRelation] = () + discrete_mechanism: discrete_mechanisms.DiscreteMechanismConfig = ( + dataclasses.field(default_factory=discrete_mechanisms.AIMConfig) + ) + numerical_bins: int = 32 + init_budget_fraction: float = 0.1 + num_permutation_slots: int = 2 + exploration_strategy: Literal['empty_token', 'size_sliced'] = 'empty_token' + + def configure( + self, + *, + zcdp_rho: float, + delta: float = 0.0, + max_records_per_user: int = 1, + ) -> MultiTableMechanism: + """Configures privacy budgets across Phase 1 and Phase 2 relational links.""" + del zcdp_rho, delta, max_records_per_user + raise NotImplementedError('configure is not yet implemented.') diff --git a/dpsynth/relational/transformations.py b/dpsynth/relational/transformations.py new file mode 100644 index 0000000..1dbe498 --- /dev/null +++ b/dpsynth/relational/transformations.py @@ -0,0 +1,184 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Pure, deterministic relational data transformers and mathematical helpers.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from typing import Literal + +from dpsynth.relational import domain as rel_domain +import mbi +import numpy as np +import pandas as pd + + +def compute_hierarchical_weights( + tables: Mapping[str, pd.DataFrame], + foreign_keys: Sequence[rel_domain.ForeignKeyRelation], +) -> dict[str, np.ndarray]: + """Computes standalone sensitivity weights (w = 1/k_eff) for Phase 1 initializers. + + Cascades group capacity truncation down the foreign key hierarchy and assigns + weights to each table such that the sum of weights associated with every root + household record equals 1.0 (unit sensitivity Delta = 1.0). + + Args: + tables: Mapping from table name to input DataFrame. + foreign_keys: Sequence of foreign key relationships between tables. + + Returns: + A dictionary mapping table name to a 1D float array of row weights. + + Raises: + ValueError: If foreign key relationships contain invalid table/column + references. + """ + del tables, foreign_keys + raise NotImplementedError( + 'compute_hierarchical_weights is not yet implemented.' + ) + + +def build_permuted_exploration_dataset( + parent_dataset: mbi.Dataset, + child_dataset: mbi.Dataset, + parent_primary_keys: Sequence[str | int] | np.ndarray, + child_foreign_keys: Sequence[str | int] | np.ndarray, + num_permutation_slots: int = 2, + strategy: Literal['empty_token', 'size_sliced'] = 'empty_token', +) -> mbi.Dataset: + """Constructs the permuted multi-slot exploration dataset for candidate selection. + + Args: + parent_dataset: Encoded discrete mbi.Dataset for the parent table. + child_dataset: Encoded discrete mbi.Dataset for the child table. + parent_primary_keys: Sequence or array of parent primary key identifiers. + child_foreign_keys: Sequence or array of child foreign key references. + num_permutation_slots: Number of permutation slots (o) in exploration table, + default 2. + strategy: Exploration strategy ('empty_token' with or + 'size_sliced'). + + Returns: + An mbi.Dataset instance representing the permuted exploration table. + + Raises: + ValueError: If strategy is unsupported or num_permutation_slots < 1. + """ + del parent_dataset, child_dataset, parent_primary_keys + del child_foreign_keys, num_permutation_slots, strategy + raise NotImplementedError( + 'build_permuted_exploration_dataset is not yet implemented.' + ) + + +def create_slot_linear_chain_constraints( + child_domain: mbi.Domain, + num_permutation_slots: int = 2, +) -> list[mbi.Constraint]: + """Creates adjacent pairwise mbi.Constraint objects for monolithic slot validity. + + For each slot, generates D-1 pairwise adjacent constraints ((S_i.A_1, + S_i.A_2), + (S_i.A_2, S_i.A_3), ...) setting log-potential to -inf on mixed states, + ensuring + sampled slots are 100% Real or 100% with bounded treewidth <= 2. + + Args: + child_domain: Sub-domain representing attributes of a single child record. + num_permutation_slots: Number of permutation slots (o), default 2. + + Returns: + A list of mbi.Constraint instances enforcing monolithic slot locking. + """ + del child_domain, num_permutation_slots + raise NotImplementedError( + 'create_slot_linear_chain_constraints is not yet implemented.' + ) + + +def symmetrize_to_wide_domain( + measurements: Sequence[mbi.LinearMeasurement], + max_children_per_parent: int, + num_permutation_slots: int = 2, +) -> list[mbi.LinearMeasurement]: + """Replicates selected exploration measurements across all generation slots. + + Equivariantly replicates candidate measurements from (S_1) and (S_1, S_2) + to all s slots and all comb(s, 2) sibling pairs in the wide generation MRF. + + Args: + measurements: Noisy marginal measurements from exploration candidate + selection. + max_children_per_parent: Maximum group capacity bound (s). + num_permutation_slots: Number of permutation exploration slots (o), default + 2. + + Returns: + A list of expanded LinearMeasurement objects for the wide generation MRF. + """ + del measurements, max_children_per_parent, num_permutation_slots + raise NotImplementedError('symmetrize_to_wide_domain is not yet implemented.') + + +def quantile_copula_coupling( + synth_parents: mbi.Dataset, + synth_wide_children: mbi.Dataset, + parent_columns: Sequence[str], + rng: np.random.Generator | None = None, +) -> mbi.Dataset: + """Couples synthetic parents and wide child records via Quantile Copula Matching. + + Applies randomized within-bin tie-breaking and lexicographical sorting along + parent feature coordinates to align parent records with wide child records. + + Args: + synth_parents: Discrete mbi.Dataset of synthesized parent records. + synth_wide_children: Discrete mbi.Dataset of synthesized wide child records. + parent_columns: Parent feature columns used as the coupling anchor. + rng: Random number generator for within-bin tie-breaking permutation. + + Returns: + The coupled wide child discrete mbi.Dataset aligned with synthetic parents. + """ + del synth_parents, synth_wide_children, parent_columns, rng + raise NotImplementedError('quantile_copula_coupling is not yet implemented.') + + +def unstack_wide_family_records( + synth_wide_dataset: mbi.Dataset, + child_domain: mbi.Domain, + max_children_per_parent: int, +) -> tuple[mbi.Dataset, np.ndarray]: + """Unstacks wide family records into a standard normalized child mbi.Dataset. + + Reads group_size = k on each wide row, emits the active child records, and + returns the unstacked child dataset along with a 1D mapping array of parent + row indices. + + Args: + synth_wide_dataset: Discrete mbi.Dataset of wide family records. + child_domain: Single-child mbi.Domain defining attribute sizes. + max_children_per_parent: Maximum group capacity bound (s). + + Returns: + A tuple of (unstacked_child_dataset, parent_row_indices) where + parent_row_indices maps each unstacked child record to its parent row. + """ + del synth_wide_dataset, child_domain, max_children_per_parent + raise NotImplementedError( + 'unstack_wide_family_records is not yet implemented.' + ) diff --git a/tests/relational/domain_test.py b/tests/relational/domain_test.py new file mode 100644 index 0000000..bbd620d --- /dev/null +++ b/tests/relational/domain_test.py @@ -0,0 +1,51 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for dpsynth.relational.domain.""" + +from absl.testing import absltest +from dpsynth.relational import domain + + +class DomainTest(absltest.TestCase): + + def test_foreign_key_relation_initialization(self): + fk = domain.ForeignKeyRelation( + parent_table='households', + parent_primary_key='household_id', + child_table='persons', + child_foreign_key='household_id', + max_children_per_parent=5, + ) + self.assertEqual(fk.parent_table, 'households') + self.assertEqual(fk.parent_primary_key, 'household_id') + self.assertEqual(fk.child_table, 'persons') + self.assertEqual(fk.child_foreign_key, 'household_id') + self.assertEqual(fk.max_children_per_parent, 5) + + def test_foreign_key_relation_invalid_capacity(self): + with self.assertRaisesRegex( + ValueError, 'max_children_per_parent must be >= 1' + ): + domain.ForeignKeyRelation( + parent_table='households', + parent_primary_key='household_id', + child_table='persons', + child_foreign_key='household_id', + max_children_per_parent=0, + ) + + +if __name__ == '__main__': + absltest.main() diff --git a/tests/relational/synthesizer_test.py b/tests/relational/synthesizer_test.py new file mode 100644 index 0000000..29178ca --- /dev/null +++ b/tests/relational/synthesizer_test.py @@ -0,0 +1,34 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for dpsynth.relational.synthesizer.""" + +from absl.testing import absltest +from dpsynth import api +from dpsynth.relational import synthesizer + + +class SynthesizerTest(absltest.TestCase): + + def test_classes_inherit_correct_api_abstractions(self): + self.assertTrue( + issubclass(synthesizer.MultiTableConfig, api.MechanismConfig) + ) + self.assertTrue( + issubclass(synthesizer.MultiTableMechanism, api.CalibratedMechanism) + ) + + +if __name__ == '__main__': + absltest.main() diff --git a/tests/relational/transformations_test.py b/tests/relational/transformations_test.py new file mode 100644 index 0000000..723562e --- /dev/null +++ b/tests/relational/transformations_test.py @@ -0,0 +1,37 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for dpsynth.relational.transformations.""" + +from absl.testing import absltest +from dpsynth.relational import transformations + + +class TransformationsTest(absltest.TestCase): + + def test_transformations_import_and_callable(self): + self.assertTrue(callable(transformations.compute_hierarchical_weights)) + self.assertTrue( + callable(transformations.build_permuted_exploration_dataset) + ) + self.assertTrue( + callable(transformations.create_slot_linear_chain_constraints) + ) + self.assertTrue(callable(transformations.symmetrize_to_wide_domain)) + self.assertTrue(callable(transformations.quantile_copula_coupling)) + self.assertTrue(callable(transformations.unstack_wide_family_records)) + + +if __name__ == '__main__': + absltest.main()