Skip to content

Combination Generators

CombinationGenerator

Bases: Protocol

Protocol for generating and counting combinations of candidate slices.

This protocol defines the interface for combination generators used in Generate-and-Test workflows. Implementations determine which k-element subsets of candidate slices should be evaluated.

Attributes:

Name Type Description
k int

Number of elements in each combination to generate.

Source code in energy_repset/combi_gens/combination_generator.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class CombinationGenerator(Protocol):
    """Protocol for generating and counting combinations of candidate slices.

    This protocol defines the interface for combination generators used in
    Generate-and-Test workflows. Implementations determine which k-element
    subsets of candidate slices should be evaluated.

    Attributes:
        k: Number of elements in each combination to generate.
    """

    k: int

    def generate(self, unique_slices: Sequence[Hashable]) -> Iterator[SliceCombination]:
        """Generate k-combinations from the candidate slices.

        Args:
            unique_slices: Sequence of candidate slice labels.

        Yields:
            Tuples of length k representing candidate selections.
        """
        ...

    def count(self, unique_slices: Sequence[Hashable]) -> int:
        """Count the total number of combinations that will be generated.

        Args:
            unique_slices: Sequence of candidate slice labels.

        Returns:
            Total number of k-combinations.
        """
        ...

    def combination_is_valid(self, combination: SliceCombination, unique_slices: Sequence[Hashable]) -> bool:
        """Check if a combination is valid according to generator constraints.

        Args:
            combination: Tuple of slice labels to validate.
            unique_slices: Sequence of candidate slice labels.

        Returns:
            True if the combination satisfies the generator's constraints.
        """
        ...

generate

generate(unique_slices: Sequence[Hashable]) -> Iterator[SliceCombination]

Generate k-combinations from the candidate slices.

Parameters:

Name Type Description Default
unique_slices Sequence[Hashable]

Sequence of candidate slice labels.

required

Yields:

Type Description
SliceCombination

Tuples of length k representing candidate selections.

Source code in energy_repset/combi_gens/combination_generator.py
22
23
24
25
26
27
28
29
30
31
def generate(self, unique_slices: Sequence[Hashable]) -> Iterator[SliceCombination]:
    """Generate k-combinations from the candidate slices.

    Args:
        unique_slices: Sequence of candidate slice labels.

    Yields:
        Tuples of length k representing candidate selections.
    """
    ...

count

count(unique_slices: Sequence[Hashable]) -> int

Count the total number of combinations that will be generated.

Parameters:

Name Type Description Default
unique_slices Sequence[Hashable]

Sequence of candidate slice labels.

required

Returns:

Type Description
int

Total number of k-combinations.

Source code in energy_repset/combi_gens/combination_generator.py
33
34
35
36
37
38
39
40
41
42
def count(self, unique_slices: Sequence[Hashable]) -> int:
    """Count the total number of combinations that will be generated.

    Args:
        unique_slices: Sequence of candidate slice labels.

    Returns:
        Total number of k-combinations.
    """
    ...

combination_is_valid

combination_is_valid(combination: SliceCombination, unique_slices: Sequence[Hashable]) -> bool

Check if a combination is valid according to generator constraints.

Parameters:

Name Type Description Default
combination SliceCombination

Tuple of slice labels to validate.

required
unique_slices Sequence[Hashable]

Sequence of candidate slice labels.

required

Returns:

Type Description
bool

True if the combination satisfies the generator's constraints.

Source code in energy_repset/combi_gens/combination_generator.py
44
45
46
47
48
49
50
51
52
53
54
def combination_is_valid(self, combination: SliceCombination, unique_slices: Sequence[Hashable]) -> bool:
    """Check if a combination is valid according to generator constraints.

    Args:
        combination: Tuple of slice labels to validate.
        unique_slices: Sequence of candidate slice labels.

    Returns:
        True if the combination satisfies the generator's constraints.
    """
    ...

ExhaustiveCombiGen

Bases: CombinationGenerator

Generate all k-combinations of the candidate slices.

This generator produces every possible k-element subset using itertools.combinations. It is suitable for small problem sizes where the total number of combinations (n choose k) is computationally feasible.

Parameters:

Name Type Description Default
k int

Number of elements in each combination.

required

Attributes:

Name Type Description
k

Number of elements per combination.

Note

The count is computed via binomial coefficient (n choose k) and matches the number of yielded combinations exactly.

Examples:

Generate all 3-month combinations from a year:

>>> from energy_repset.combi_gens import ExhaustiveCombiGen
>>> import pandas as pd
>>>
>>> months = [pd.Period('2024-01', 'M'), pd.Period('2024-02', 'M'),
...           pd.Period('2024-03', 'M'), pd.Period('2024-04', 'M')]
>>> generator = ExhaustiveCombiGen(k=3)
>>> generator.count(months)  # 4 choose 3
    4
>>> list(generator.generate(months))
    [(Period('2024-01', 'M'), Period('2024-02', 'M'), Period('2024-03', 'M')),
     (Period('2024-01', 'M'), Period('2024-02', 'M'), Period('2024-04', 'M')),
     (Period('2024-01', 'M'), Period('2024-03', 'M'), Period('2024-04', 'M')),
     (Period('2024-02', 'M'), Period('2024-03', 'M'), Period('2024-04', 'M'))]
Source code in energy_repset/combi_gens/simple_exhaustive.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
class ExhaustiveCombiGen(CombinationGenerator):
    """Generate all k-combinations of the candidate slices.

    This generator produces every possible k-element subset using
    itertools.combinations. It is suitable for small problem sizes where
    the total number of combinations (n choose k) is computationally feasible.

    Args:
        k: Number of elements in each combination.

    Attributes:
        k: Number of elements per combination.

    Note:
        The count is computed via binomial coefficient (n choose k) and matches
        the number of yielded combinations exactly.

    Examples:
        Generate all 3-month combinations from a year:

        >>> from energy_repset.combi_gens import ExhaustiveCombiGen
        >>> import pandas as pd
        >>>
        >>> months = [pd.Period('2024-01', 'M'), pd.Period('2024-02', 'M'),
        ...           pd.Period('2024-03', 'M'), pd.Period('2024-04', 'M')]
        >>> generator = ExhaustiveCombiGen(k=3)
        >>> generator.count(months)  # 4 choose 3
            4
        >>> list(generator.generate(months))
            [(Period('2024-01', 'M'), Period('2024-02', 'M'), Period('2024-03', 'M')),
             (Period('2024-01', 'M'), Period('2024-02', 'M'), Period('2024-04', 'M')),
             (Period('2024-01', 'M'), Period('2024-03', 'M'), Period('2024-04', 'M')),
             (Period('2024-02', 'M'), Period('2024-03', 'M'), Period('2024-04', 'M'))]
    """

    def __init__(self, k: int) -> None:
        """Initialize exhaustive generator with target combination size.

        Args:
            k: Number of elements in each combination.
        """
        self.k = k

    def generate(self, unique_slices: Sequence[Hashable]) -> Iterator[SliceCombination]:
        """Generate all k-combinations using itertools.combinations.

        Args:
            unique_slices: Sequence of candidate slice labels.

        Yields:
            All possible k-element tuples from unique_slices.
        """
        yield from itertools.combinations(unique_slices, self.k)

    def count(self, unique_slices: Sequence[Hashable]) -> int:
        """Count total combinations using binomial coefficient.

        Args:
            unique_slices: Sequence of candidate slice labels.

        Returns:
            n choose k, where n is the number of unique slices.
        """
        return math.comb(len(unique_slices), self.k)

    def combination_is_valid(self, combination: SliceCombination, unique_slices: Sequence[Hashable]) -> bool:
        """Check if combination has exactly k elements from unique_slices.

        Args:
            combination: Tuple of slice labels to validate.
            unique_slices: Sequence of candidate slice labels.

        Returns:
            True if combination has k elements all in unique_slices.
        """
        if len(combination) != self.k:
            return False
        if any(s not in unique_slices for s in combination):
            return False
        return True

__init__

__init__(k: int) -> None

Initialize exhaustive generator with target combination size.

Parameters:

Name Type Description Default
k int

Number of elements in each combination.

required
Source code in energy_repset/combi_gens/simple_exhaustive.py
48
49
50
51
52
53
54
def __init__(self, k: int) -> None:
    """Initialize exhaustive generator with target combination size.

    Args:
        k: Number of elements in each combination.
    """
    self.k = k

generate

generate(unique_slices: Sequence[Hashable]) -> Iterator[SliceCombination]

Generate all k-combinations using itertools.combinations.

Parameters:

Name Type Description Default
unique_slices Sequence[Hashable]

Sequence of candidate slice labels.

required

Yields:

Type Description
SliceCombination

All possible k-element tuples from unique_slices.

Source code in energy_repset/combi_gens/simple_exhaustive.py
56
57
58
59
60
61
62
63
64
65
def generate(self, unique_slices: Sequence[Hashable]) -> Iterator[SliceCombination]:
    """Generate all k-combinations using itertools.combinations.

    Args:
        unique_slices: Sequence of candidate slice labels.

    Yields:
        All possible k-element tuples from unique_slices.
    """
    yield from itertools.combinations(unique_slices, self.k)

count

count(unique_slices: Sequence[Hashable]) -> int

Count total combinations using binomial coefficient.

Parameters:

Name Type Description Default
unique_slices Sequence[Hashable]

Sequence of candidate slice labels.

required

Returns:

Type Description
int

n choose k, where n is the number of unique slices.

Source code in energy_repset/combi_gens/simple_exhaustive.py
67
68
69
70
71
72
73
74
75
76
def count(self, unique_slices: Sequence[Hashable]) -> int:
    """Count total combinations using binomial coefficient.

    Args:
        unique_slices: Sequence of candidate slice labels.

    Returns:
        n choose k, where n is the number of unique slices.
    """
    return math.comb(len(unique_slices), self.k)

combination_is_valid

combination_is_valid(combination: SliceCombination, unique_slices: Sequence[Hashable]) -> bool

Check if combination has exactly k elements from unique_slices.

Parameters:

Name Type Description Default
combination SliceCombination

Tuple of slice labels to validate.

required
unique_slices Sequence[Hashable]

Sequence of candidate slice labels.

required

Returns:

Type Description
bool

True if combination has k elements all in unique_slices.

Source code in energy_repset/combi_gens/simple_exhaustive.py
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def combination_is_valid(self, combination: SliceCombination, unique_slices: Sequence[Hashable]) -> bool:
    """Check if combination has exactly k elements from unique_slices.

    Args:
        combination: Tuple of slice labels to validate.
        unique_slices: Sequence of candidate slice labels.

    Returns:
        True if combination has k elements all in unique_slices.
    """
    if len(combination) != self.k:
        return False
    if any(s not in unique_slices for s in combination):
        return False
    return True

GroupQuotaCombiGen

Bases: CombinationGenerator

Generate combinations that respect exact quotas per group.

This generator enforces that selections contain a specific number of elements from each group. It is useful for ensuring balanced representation across categories (e.g., seasons, must-have periods, etc.).

Parameters:

Name Type Description Default
k int

Total number of elements in each combination. Must equal sum of group quotas.

required
slice_to_group_mapping dict[Hashable, Hashable]

Mapping from each candidate slice to its group label.

required
group_quota dict[Hashable, int]

Mapping from group label to the required count in the selection.

required

Attributes:

Name Type Description
k

Number of elements per combination.

group_of

Mapping from slice to group.

group_quota

Required count per group.

Raises:

Type Description
ValueError

If sum of group quotas does not equal k.

Note

Use this to enforce constraints like "exactly one month per season" or "2 must-have periods plus 2 optional periods".

Examples:

Example 1 - Seasonal constraints (one month per season):

>>> from energy_repset.combi_gens import GroupQuotaCombiGen
>>> import pandas as pd
>>>
>>> # Define months and their seasons
>>> months = [pd.Period(f'2024-{i:02d}', 'M') for i in range(1, 13)]
>>> season_map = {}
>>> for month in months:
...     if month.month in [12, 1, 2]: season_map[month] = 'winter'
...     elif month.month in [3, 4, 5]: season_map[month] = 'spring'
...     elif month.month in [6, 7, 8]: season_map[month] = 'summer'
...     else: season_map[month] = 'fall'
>>>
>>> # Select 4 months, one per season
>>> generator = GroupQuotaCombiGen(
...     k=4,
...     slice_to_group_mapping=season_map,
...     group_quota={'winter': 1, 'spring': 1, 'summer': 1, 'fall': 1}
... )
>>> generator.count(months)  # 3 * 3 * 3 * 3 = 81 combinations
    81

Example 2 - Optional and must-have categories:

>>> # Force specific periods to be included
>>> months = [pd.Period(f'2024-{i:02d}', 'M') for i in range(1, 13)]
>>> group_mapping = {p: 'optional' for p in months}
>>> group_mapping[pd.Period('2024-01', 'M')] = 'must'
>>> group_mapping[pd.Period('2024-12', 'M')] = 'must'
>>>
>>> # Select 4 total: 2 must-have + 2 optional
>>> generator = GroupQuotaCombiGen(
...     k=4,
...     slice_to_group_mapping=group_mapping,
...     group_quota={'optional': 2, 'must': 2}
... )
>>> # All combinations will include Jan and Dec plus 2 from the other 10
>>> generator.count(months)  # 1 * 45 = 45 combinations
45
Source code in energy_repset/combi_gens/simple_group_quota.py
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
class GroupQuotaCombiGen(CombinationGenerator):
    """Generate combinations that respect exact quotas per group.

    This generator enforces that selections contain a specific number of elements
    from each group. It is useful for ensuring balanced representation across
    categories (e.g., seasons, must-have periods, etc.).

    Args:
        k: Total number of elements in each combination. Must equal sum of group quotas.
        slice_to_group_mapping: Mapping from each candidate slice to its group label.
        group_quota: Mapping from group label to the required count in the selection.

    Attributes:
        k: Number of elements per combination.
        group_of: Mapping from slice to group.
        group_quota: Required count per group.

    Raises:
        ValueError: If sum of group quotas does not equal k.

    Note:
        Use this to enforce constraints like "exactly one month per season" or
        "2 must-have periods plus 2 optional periods".

    Examples:
        Example 1 - Seasonal constraints (one month per season):

        >>> from energy_repset.combi_gens import GroupQuotaCombiGen
        >>> import pandas as pd
        >>>
        >>> # Define months and their seasons
        >>> months = [pd.Period(f'2024-{i:02d}', 'M') for i in range(1, 13)]
        >>> season_map = {}
        >>> for month in months:
        ...     if month.month in [12, 1, 2]: season_map[month] = 'winter'
        ...     elif month.month in [3, 4, 5]: season_map[month] = 'spring'
        ...     elif month.month in [6, 7, 8]: season_map[month] = 'summer'
        ...     else: season_map[month] = 'fall'
        >>>
        >>> # Select 4 months, one per season
        >>> generator = GroupQuotaCombiGen(
        ...     k=4,
        ...     slice_to_group_mapping=season_map,
        ...     group_quota={'winter': 1, 'spring': 1, 'summer': 1, 'fall': 1}
        ... )
        >>> generator.count(months)  # 3 * 3 * 3 * 3 = 81 combinations
            81

        Example 2 - Optional and must-have categories:

        >>> # Force specific periods to be included
        >>> months = [pd.Period(f'2024-{i:02d}', 'M') for i in range(1, 13)]
        >>> group_mapping = {p: 'optional' for p in months}
        >>> group_mapping[pd.Period('2024-01', 'M')] = 'must'
        >>> group_mapping[pd.Period('2024-12', 'M')] = 'must'
        >>>
        >>> # Select 4 total: 2 must-have + 2 optional
        >>> generator = GroupQuotaCombiGen(
        ...     k=4,
        ...     slice_to_group_mapping=group_mapping,
        ...     group_quota={'optional': 2, 'must': 2}
        ... )
        >>> # All combinations will include Jan and Dec plus 2 from the other 10
        >>> generator.count(months)  # 1 * 45 = 45 combinations
        45
    """

    def __init__(
            self,
            k: int,
            slice_to_group_mapping: Dict[Hashable, Hashable],
            group_quota: Dict[Hashable, int]
    ) -> None:
        """Initialize generator with group quotas.

        Args:
            k: Total number of elements in each combination.
            slice_to_group_mapping: Mapping from slice to its group label.
            group_quota: Required count per group.

        Raises:
            ValueError: If sum of group quotas does not equal k.
        """
        self.k = k
        self.group_of = slice_to_group_mapping
        self.group_quota = group_quota

        # Validate that quotas sum to k
        if sum(group_quota.values()) != k:
            raise ValueError(f"Sum of group quotas ({sum(group_quota.values())}) must equal k ({k})")

    def generate(self, unique_slices: Sequence[Hashable]) -> Iterator[SliceCombination]:
        """Generate combinations respecting group quotas.

        Args:
            unique_slices: Sequence of candidate slice labels.

        Yields:
            Tuples of length k where each group contributes exactly its quota.
        """
        groups: Dict[Hashable, List[Hashable]] = {}
        for c in unique_slices:
            g = self.group_of[c]
            groups.setdefault(g, []).append(c)
        per_group_lists = []
        for g, q in self.group_quota.items():
            per_group_lists.append(list(itertools.combinations(groups.get(g, []), q)))
        for tpl in itertools.product(*per_group_lists):
            flat = tuple(itertools.chain.from_iterable(tpl))
            if len(flat) == self.k:
                yield flat

    def count(self, unique_slices: Sequence[Hashable]) -> int:
        """Count total combinations respecting group quotas.

        Args:
            unique_slices: Sequence of candidate slice labels.

        Returns:
            Product of binomial coefficients across all groups. For each group
            with n members and quota q, contributes C(n, q) to the product.
        """
        groups: Dict[Hashable, List[Hashable]] = {}
        for c in unique_slices:
            g = self.group_of[c]
            groups.setdefault(g, []).append(c)
        total = 1
        for g, q in self.group_quota.items():
            n = len(groups.get(g, []))
            total *= math.comb(n, q)
        return total

    def combination_is_valid(self, combination: SliceCombination, unique_slices: Sequence[Hashable]) -> bool:
        """Check if combination satisfies group quotas.

        Args:
            combination: Tuple of slice labels to validate.
            unique_slices: Sequence of candidate slice labels.

        Returns:
            True if combination has exactly k elements with each group contributing
            its required quota.
        """
        if len(combination) != self.k:
            return False
        if any(s not in unique_slices for s in combination):
            return False

        group_count = {group_name: 0 for group_name in self.group_quota.keys()}
        for slice in combination:
            group_count[self.group_of[slice]] +=1
        if not all(group_count[g] == self.group_quota[g] for g in self.group_quota.keys()):
            return False

        return True

__init__

__init__(k: int, slice_to_group_mapping: dict[Hashable, Hashable], group_quota: dict[Hashable, int]) -> None

Initialize generator with group quotas.

Parameters:

Name Type Description Default
k int

Total number of elements in each combination.

required
slice_to_group_mapping dict[Hashable, Hashable]

Mapping from slice to its group label.

required
group_quota dict[Hashable, int]

Required count per group.

required

Raises:

Type Description
ValueError

If sum of group quotas does not equal k.

Source code in energy_repset/combi_gens/simple_group_quota.py
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def __init__(
        self,
        k: int,
        slice_to_group_mapping: Dict[Hashable, Hashable],
        group_quota: Dict[Hashable, int]
) -> None:
    """Initialize generator with group quotas.

    Args:
        k: Total number of elements in each combination.
        slice_to_group_mapping: Mapping from slice to its group label.
        group_quota: Required count per group.

    Raises:
        ValueError: If sum of group quotas does not equal k.
    """
    self.k = k
    self.group_of = slice_to_group_mapping
    self.group_quota = group_quota

    # Validate that quotas sum to k
    if sum(group_quota.values()) != k:
        raise ValueError(f"Sum of group quotas ({sum(group_quota.values())}) must equal k ({k})")

generate

generate(unique_slices: Sequence[Hashable]) -> Iterator[SliceCombination]

Generate combinations respecting group quotas.

Parameters:

Name Type Description Default
unique_slices Sequence[Hashable]

Sequence of candidate slice labels.

required

Yields:

Type Description
SliceCombination

Tuples of length k where each group contributes exactly its quota.

Source code in energy_repset/combi_gens/simple_group_quota.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
def generate(self, unique_slices: Sequence[Hashable]) -> Iterator[SliceCombination]:
    """Generate combinations respecting group quotas.

    Args:
        unique_slices: Sequence of candidate slice labels.

    Yields:
        Tuples of length k where each group contributes exactly its quota.
    """
    groups: Dict[Hashable, List[Hashable]] = {}
    for c in unique_slices:
        g = self.group_of[c]
        groups.setdefault(g, []).append(c)
    per_group_lists = []
    for g, q in self.group_quota.items():
        per_group_lists.append(list(itertools.combinations(groups.get(g, []), q)))
    for tpl in itertools.product(*per_group_lists):
        flat = tuple(itertools.chain.from_iterable(tpl))
        if len(flat) == self.k:
            yield flat

count

count(unique_slices: Sequence[Hashable]) -> int

Count total combinations respecting group quotas.

Parameters:

Name Type Description Default
unique_slices Sequence[Hashable]

Sequence of candidate slice labels.

required

Returns:

Type Description
int

Product of binomial coefficients across all groups. For each group

int

with n members and quota q, contributes C(n, q) to the product.

Source code in energy_repset/combi_gens/simple_group_quota.py
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
def count(self, unique_slices: Sequence[Hashable]) -> int:
    """Count total combinations respecting group quotas.

    Args:
        unique_slices: Sequence of candidate slice labels.

    Returns:
        Product of binomial coefficients across all groups. For each group
        with n members and quota q, contributes C(n, q) to the product.
    """
    groups: Dict[Hashable, List[Hashable]] = {}
    for c in unique_slices:
        g = self.group_of[c]
        groups.setdefault(g, []).append(c)
    total = 1
    for g, q in self.group_quota.items():
        n = len(groups.get(g, []))
        total *= math.comb(n, q)
    return total

combination_is_valid

combination_is_valid(combination: SliceCombination, unique_slices: Sequence[Hashable]) -> bool

Check if combination satisfies group quotas.

Parameters:

Name Type Description Default
combination SliceCombination

Tuple of slice labels to validate.

required
unique_slices Sequence[Hashable]

Sequence of candidate slice labels.

required

Returns:

Type Description
bool

True if combination has exactly k elements with each group contributing

bool

its required quota.

Source code in energy_repset/combi_gens/simple_group_quota.py
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
def combination_is_valid(self, combination: SliceCombination, unique_slices: Sequence[Hashable]) -> bool:
    """Check if combination satisfies group quotas.

    Args:
        combination: Tuple of slice labels to validate.
        unique_slices: Sequence of candidate slice labels.

    Returns:
        True if combination has exactly k elements with each group contributing
        its required quota.
    """
    if len(combination) != self.k:
        return False
    if any(s not in unique_slices for s in combination):
        return False

    group_count = {group_name: 0 for group_name in self.group_quota.keys()}
    for slice in combination:
        group_count[self.group_of[slice]] +=1
    if not all(group_count[g] == self.group_quota[g] for g in self.group_quota.keys()):
        return False

    return True

ExhaustiveHierarchicalCombiGen

Bases: CombinationGenerator

Generate combinations where child slices are selected in complete parent groups.

This generator enforces hierarchical selection: child slices (e.g., days) can only be selected as complete parent groups (e.g., months). It enables high-resolution features (e.g. per-day) while enforcing structural constraints at the parent level (e.g. months).

Parameters:

Name Type Description Default
parent_k int

Number of parent groups to select.

required
slice_to_parent_mapping dict[Hashable, Hashable]

Mapping from each child slice to its parent group. Example: {Period('2024-01-01', 'D'): Period('2024-01', 'M'), ...}

required

Attributes:

Name Type Description
k

Number of parent groups per combination (same as parent_k for protocol compliance).

parent_k

Number of parent groups per combination.

slice_to_parent

Child to parent mapping.

Note

The generate() method yields flattened tuples of child slices, but internally enforces parent-level constraints. Use the factory method from_slicers() for automatic parent grouping based on TimeSlicer objects.

Examples:

Manual construction with custom grouping:

>>> from energy_repset.combi_gens import ExhaustiveHierarchicalCombiGen
>>> import pandas as pd
>>>
>>> # Define child-to-parent mapping
>>> slice_to_parent = {
...     pd.Period('2024-01-01', 'D'): pd.Period('2024-01', 'M'),
...     pd.Period('2024-01-02', 'D'): pd.Period('2024-01', 'M'),
...     pd.Period('2024-02-01', 'D'): pd.Period('2024-02', 'M'),
...     pd.Period('2024-02-02', 'D'): pd.Period('2024-02', 'M'),
... }
>>>
>>> # Select 2 months, but combinations contain days
>>> gen = ExhaustiveHierarchicalCombiGen(
...     parent_k=2,
...     slice_to_parent_mapping=slice_to_parent
... )
>>> gen.count(list(slice_to_parent.keys()))  # C(2, 2) = 1
    1

Using factory method with TimeSlicer:

>>> import pandas as pd
>>> from energy_repset.time_slicer import TimeSlicer
>>> from energy_repset.combi_gens import ExhaustiveHierarchicalCombiGen
>>>
>>> dates = pd.date_range('2024-01-01', periods=366, freq='D')
>>> child_slicer = TimeSlicer(unit='day')
>>> parent_slicer = TimeSlicer(unit='month')
>>>
>>> gen = ExhaustiveHierarchicalCombiGen.from_slicers(
...     parent_k=3,
...     dt_index=dates,
...     child_slicer=child_slicer,
...     parent_slicer=parent_slicer
... )
>>> unique_days = child_slicer.unique_slices(dates)
>>> gen.count(unique_days)  # C(12, 3) = 220 combinations of months
    220
Source code in energy_repset/combi_gens/hierarchical_exhaustive.py
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
class ExhaustiveHierarchicalCombiGen(CombinationGenerator):
    """Generate combinations where child slices are selected in complete parent groups.

    This generator enforces hierarchical selection: child slices (e.g., days) can only
    be selected as complete parent groups (e.g., months). It enables high-resolution
    features (e.g. per-day) while enforcing structural constraints at the parent level (e.g. months).

    Args:
        parent_k: Number of parent groups to select.
        slice_to_parent_mapping: Mapping from each child slice to its parent group.
            Example: {Period('2024-01-01', 'D'): Period('2024-01', 'M'), ...}

    Attributes:
        k: Number of parent groups per combination (same as parent_k for protocol compliance).
        parent_k: Number of parent groups per combination.
        slice_to_parent: Child to parent mapping.

    Note:
        The `generate()` method yields flattened tuples of child slices, but internally
        enforces parent-level constraints. Use the factory method `from_slicers()`
        for automatic parent grouping based on TimeSlicer objects.

    Examples:
        Manual construction with custom grouping:

        >>> from energy_repset.combi_gens import ExhaustiveHierarchicalCombiGen
        >>> import pandas as pd
        >>>
        >>> # Define child-to-parent mapping
        >>> slice_to_parent = {
        ...     pd.Period('2024-01-01', 'D'): pd.Period('2024-01', 'M'),
        ...     pd.Period('2024-01-02', 'D'): pd.Period('2024-01', 'M'),
        ...     pd.Period('2024-02-01', 'D'): pd.Period('2024-02', 'M'),
        ...     pd.Period('2024-02-02', 'D'): pd.Period('2024-02', 'M'),
        ... }
        >>>
        >>> # Select 2 months, but combinations contain days
        >>> gen = ExhaustiveHierarchicalCombiGen(
        ...     parent_k=2,
        ...     slice_to_parent_mapping=slice_to_parent
        ... )
        >>> gen.count(list(slice_to_parent.keys()))  # C(2, 2) = 1
            1

        Using factory method with TimeSlicer:

        >>> import pandas as pd
        >>> from energy_repset.time_slicer import TimeSlicer
        >>> from energy_repset.combi_gens import ExhaustiveHierarchicalCombiGen
        >>>
        >>> dates = pd.date_range('2024-01-01', periods=366, freq='D')
        >>> child_slicer = TimeSlicer(unit='day')
        >>> parent_slicer = TimeSlicer(unit='month')
        >>>
        >>> gen = ExhaustiveHierarchicalCombiGen.from_slicers(
        ...     parent_k=3,
        ...     dt_index=dates,
        ...     child_slicer=child_slicer,
        ...     parent_slicer=parent_slicer
        ... )
        >>> unique_days = child_slicer.unique_slices(dates)
        >>> gen.count(unique_days)  # C(12, 3) = 220 combinations of months
            220
    """

    def __init__(
        self,
        parent_k: int,
        slice_to_parent_mapping: Dict[Hashable, Hashable]
    ) -> None:
        """Initialize hierarchical generator with child-to-parent mapping.

        Args:
            parent_k: Number of parent groups to select.
            slice_to_parent_mapping: Dict mapping each child slice to its parent.
        """
        self.parent_k = parent_k
        self.k = parent_k  # For CombinationGenerator protocol compliance
        self.slice_to_parent = slice_to_parent_mapping

    @classmethod
    def from_slicers(
        cls,
        parent_k: int,
        dt_index: pd.DatetimeIndex,
        child_slicer: TimeSlicer,
        parent_slicer: TimeSlicer
    ) -> ExhaustiveHierarchicalCombiGen:
        """Factory method to create generator from child and parent TimeSlicer objects.

        Args:
            parent_k: Number of parent groups to select.
            dt_index: DatetimeIndex of the time series data.
            child_slicer: TimeSlicer defining child slice granularity (e.g., daily).
            parent_slicer: TimeSlicer defining parent slice granularity (e.g., monthly).

        Returns:
            ExhaustiveHierarchicalCombinationGenerator with auto-constructed mappings.

        Examples:
            Select 4 months from a year of daily data:

            >>> import pandas as pd
            >>> from energy_repset.time_slicer import TimeSlicer
            >>> from energy_repset.combi_gens import ExhaustiveHierarchicalCombiGen
            >>>
            >>> dates = pd.date_range('2024-01-01', periods=366, freq='D')
            >>> child_slicer = TimeSlicer(unit='day')
            >>> parent_slicer = TimeSlicer(unit='month')
            >>>
            >>> gen = ExhaustiveHierarchicalCombiGen.from_slicers(
            ...     parent_k=4,
            ...     dt_index=dates,
            ...     child_slicer=child_slicer,
            ...     parent_slicer=parent_slicer
            ... )
            >>> gen.count(child_slicer.unique_slices(dates))  # C(12, 4) = 495
            495
        """
        child_labels = child_slicer.labels_for_index(dt_index)
        parent_labels = parent_slicer.labels_for_index(dt_index)

        slice_to_parent = {}
        for child, parent in zip(child_labels, parent_labels):
            slice_to_parent[child] = parent

        unique_slice_to_parent = {child: slice_to_parent[child] for child in child_labels.unique()}

        return cls(parent_k=parent_k, slice_to_parent_mapping=unique_slice_to_parent)

    def generate(self, unique_slices: Sequence[Hashable]) -> Iterator[SliceCombination]:
        """Generate combinations of k parent groups, yielding flattened child slices.

        Args:
            unique_slices: Sequence of child slice labels.

        Yields:
            Tuples containing all child slices from k selected parent groups.
        """
        parent_to_children: Dict[Hashable, list] = {}
        for child in unique_slices:
            parent = self.slice_to_parent[child]
            parent_to_children.setdefault(parent, []).append(child)

        parent_ids = sorted(parent_to_children.keys())
        for parent_combi in itertools.combinations(parent_ids, self.parent_k):
            child_slices = []
            for parent_id in sorted(parent_combi):
                child_slices.extend(parent_to_children[parent_id])
            yield tuple(child_slices)

    def count(self, unique_slices: Sequence[Hashable]) -> int:
        """Count total number of parent-level combinations.

        Args:
            unique_slices: Sequence of child slice labels.

        Returns:
            C(n_parents, parent_k) where n_parents is the number of unique parent groups.
        """
        unique_parents = set(self.slice_to_parent[s] for s in unique_slices)
        n_parents = len(unique_parents)
        return math.comb(n_parents, self.parent_k)

    def combination_is_valid(
        self,
        combination: SliceCombination,
        unique_slices: Sequence[Hashable]
    ) -> bool:
        """Check if combination represents exactly parent_k complete parent groups.

        Args:
            combination: Tuple of child slice labels to validate.
            unique_slices: Sequence of all valid child slice labels.

        Returns:
            True if combination contains all children from exactly parent_k parent groups.
        """
        if not all(c in unique_slices for c in combination):
            return False

        parent_to_children: Dict[Hashable, set] = {}
        for child in unique_slices:
            parent = self.slice_to_parent[child]
            parent_to_children.setdefault(parent, set()).add(child)

        parents_in_combi = set()
        for child in combination:
            if child not in self.slice_to_parent:
                return False
            parents_in_combi.add(self.slice_to_parent[child])

        if len(parents_in_combi) != self.parent_k:
            return False

        expected_children = set()
        for parent in parents_in_combi:
            expected_children.update(parent_to_children[parent])

        return set(combination) == expected_children

__init__

__init__(parent_k: int, slice_to_parent_mapping: dict[Hashable, Hashable]) -> None

Initialize hierarchical generator with child-to-parent mapping.

Parameters:

Name Type Description Default
parent_k int

Number of parent groups to select.

required
slice_to_parent_mapping dict[Hashable, Hashable]

Dict mapping each child slice to its parent.

required
Source code in energy_repset/combi_gens/hierarchical_exhaustive.py
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def __init__(
    self,
    parent_k: int,
    slice_to_parent_mapping: Dict[Hashable, Hashable]
) -> None:
    """Initialize hierarchical generator with child-to-parent mapping.

    Args:
        parent_k: Number of parent groups to select.
        slice_to_parent_mapping: Dict mapping each child slice to its parent.
    """
    self.parent_k = parent_k
    self.k = parent_k  # For CombinationGenerator protocol compliance
    self.slice_to_parent = slice_to_parent_mapping

from_slicers classmethod

from_slicers(parent_k: int, dt_index: DatetimeIndex, child_slicer: TimeSlicer, parent_slicer: TimeSlicer) -> ExhaustiveHierarchicalCombiGen

Factory method to create generator from child and parent TimeSlicer objects.

Parameters:

Name Type Description Default
parent_k int

Number of parent groups to select.

required
dt_index DatetimeIndex

DatetimeIndex of the time series data.

required
child_slicer TimeSlicer

TimeSlicer defining child slice granularity (e.g., daily).

required
parent_slicer TimeSlicer

TimeSlicer defining parent slice granularity (e.g., monthly).

required

Returns:

Type Description
ExhaustiveHierarchicalCombiGen

ExhaustiveHierarchicalCombinationGenerator with auto-constructed mappings.

Examples:

Select 4 months from a year of daily data:

>>> import pandas as pd
>>> from energy_repset.time_slicer import TimeSlicer
>>> from energy_repset.combi_gens import ExhaustiveHierarchicalCombiGen
>>>
>>> dates = pd.date_range('2024-01-01', periods=366, freq='D')
>>> child_slicer = TimeSlicer(unit='day')
>>> parent_slicer = TimeSlicer(unit='month')
>>>
>>> gen = ExhaustiveHierarchicalCombiGen.from_slicers(
...     parent_k=4,
...     dt_index=dates,
...     child_slicer=child_slicer,
...     parent_slicer=parent_slicer
... )
>>> gen.count(child_slicer.unique_slices(dates))  # C(12, 4) = 495
495
Source code in energy_repset/combi_gens/hierarchical_exhaustive.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
@classmethod
def from_slicers(
    cls,
    parent_k: int,
    dt_index: pd.DatetimeIndex,
    child_slicer: TimeSlicer,
    parent_slicer: TimeSlicer
) -> ExhaustiveHierarchicalCombiGen:
    """Factory method to create generator from child and parent TimeSlicer objects.

    Args:
        parent_k: Number of parent groups to select.
        dt_index: DatetimeIndex of the time series data.
        child_slicer: TimeSlicer defining child slice granularity (e.g., daily).
        parent_slicer: TimeSlicer defining parent slice granularity (e.g., monthly).

    Returns:
        ExhaustiveHierarchicalCombinationGenerator with auto-constructed mappings.

    Examples:
        Select 4 months from a year of daily data:

        >>> import pandas as pd
        >>> from energy_repset.time_slicer import TimeSlicer
        >>> from energy_repset.combi_gens import ExhaustiveHierarchicalCombiGen
        >>>
        >>> dates = pd.date_range('2024-01-01', periods=366, freq='D')
        >>> child_slicer = TimeSlicer(unit='day')
        >>> parent_slicer = TimeSlicer(unit='month')
        >>>
        >>> gen = ExhaustiveHierarchicalCombiGen.from_slicers(
        ...     parent_k=4,
        ...     dt_index=dates,
        ...     child_slicer=child_slicer,
        ...     parent_slicer=parent_slicer
        ... )
        >>> gen.count(child_slicer.unique_slices(dates))  # C(12, 4) = 495
        495
    """
    child_labels = child_slicer.labels_for_index(dt_index)
    parent_labels = parent_slicer.labels_for_index(dt_index)

    slice_to_parent = {}
    for child, parent in zip(child_labels, parent_labels):
        slice_to_parent[child] = parent

    unique_slice_to_parent = {child: slice_to_parent[child] for child in child_labels.unique()}

    return cls(parent_k=parent_k, slice_to_parent_mapping=unique_slice_to_parent)

generate

generate(unique_slices: Sequence[Hashable]) -> Iterator[SliceCombination]

Generate combinations of k parent groups, yielding flattened child slices.

Parameters:

Name Type Description Default
unique_slices Sequence[Hashable]

Sequence of child slice labels.

required

Yields:

Type Description
SliceCombination

Tuples containing all child slices from k selected parent groups.

Source code in energy_repset/combi_gens/hierarchical_exhaustive.py
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
def generate(self, unique_slices: Sequence[Hashable]) -> Iterator[SliceCombination]:
    """Generate combinations of k parent groups, yielding flattened child slices.

    Args:
        unique_slices: Sequence of child slice labels.

    Yields:
        Tuples containing all child slices from k selected parent groups.
    """
    parent_to_children: Dict[Hashable, list] = {}
    for child in unique_slices:
        parent = self.slice_to_parent[child]
        parent_to_children.setdefault(parent, []).append(child)

    parent_ids = sorted(parent_to_children.keys())
    for parent_combi in itertools.combinations(parent_ids, self.parent_k):
        child_slices = []
        for parent_id in sorted(parent_combi):
            child_slices.extend(parent_to_children[parent_id])
        yield tuple(child_slices)

count

count(unique_slices: Sequence[Hashable]) -> int

Count total number of parent-level combinations.

Parameters:

Name Type Description Default
unique_slices Sequence[Hashable]

Sequence of child slice labels.

required

Returns:

Type Description
int

C(n_parents, parent_k) where n_parents is the number of unique parent groups.

Source code in energy_repset/combi_gens/hierarchical_exhaustive.py
166
167
168
169
170
171
172
173
174
175
176
177
def count(self, unique_slices: Sequence[Hashable]) -> int:
    """Count total number of parent-level combinations.

    Args:
        unique_slices: Sequence of child slice labels.

    Returns:
        C(n_parents, parent_k) where n_parents is the number of unique parent groups.
    """
    unique_parents = set(self.slice_to_parent[s] for s in unique_slices)
    n_parents = len(unique_parents)
    return math.comb(n_parents, self.parent_k)

combination_is_valid

combination_is_valid(combination: SliceCombination, unique_slices: Sequence[Hashable]) -> bool

Check if combination represents exactly parent_k complete parent groups.

Parameters:

Name Type Description Default
combination SliceCombination

Tuple of child slice labels to validate.

required
unique_slices Sequence[Hashable]

Sequence of all valid child slice labels.

required

Returns:

Type Description
bool

True if combination contains all children from exactly parent_k parent groups.

Source code in energy_repset/combi_gens/hierarchical_exhaustive.py
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
def combination_is_valid(
    self,
    combination: SliceCombination,
    unique_slices: Sequence[Hashable]
) -> bool:
    """Check if combination represents exactly parent_k complete parent groups.

    Args:
        combination: Tuple of child slice labels to validate.
        unique_slices: Sequence of all valid child slice labels.

    Returns:
        True if combination contains all children from exactly parent_k parent groups.
    """
    if not all(c in unique_slices for c in combination):
        return False

    parent_to_children: Dict[Hashable, set] = {}
    for child in unique_slices:
        parent = self.slice_to_parent[child]
        parent_to_children.setdefault(parent, set()).add(child)

    parents_in_combi = set()
    for child in combination:
        if child not in self.slice_to_parent:
            return False
        parents_in_combi.add(self.slice_to_parent[child])

    if len(parents_in_combi) != self.parent_k:
        return False

    expected_children = set()
    for parent in parents_in_combi:
        expected_children.update(parent_to_children[parent])

    return set(combination) == expected_children

GroupQuotaHierarchicalCombiGen

Bases: CombinationGenerator

Generate combinations respecting quotas per parent-level group.

This generator combines hierarchical selection (child slices selected in complete parent groups) with group quotas (e.g., exactly 1 month per season). It enables high-resolution features (e.g. per-day) while enforcing structural constraints at the parent level (e.g. months).

Parameters:

Name Type Description Default
parent_k int

Total number of parent groups to select. Must equal sum of group quotas.

required
slice_to_parent_mapping dict[Hashable, Hashable]

Mapping from each child slice to its parent group.

required
parent_to_group_mapping dict[Hashable, Hashable]

Mapping from parent ID to its group label (e.g., season).

required
group_quota dict[Hashable, int]

Required count of parents per group.

required

Attributes:

Name Type Description
k

Number of parent groups per combination (same as parent_k for protocol compliance).

slice_to_parent

Child to parent mapping.

parent_to_group

Parent to group label mapping.

group_quota

Required count per group.

Raises:

Type Description
ValueError

If sum of group quotas does not equal parent_k.

Note

Use factory methods from_slicers() for automatic parent mapping and from_slicers_with_seasons() for automatic seasonal grouping.

Examples:

Manual construction for seasonal month selection:

>>> from energy_repset.combi_gens import GroupQuotaHierarchicalCombiGen
>>> import pandas as pd
>>>
>>> slice_to_parent = {
...     pd.Period('2024-01-01', 'D'): pd.Period('2024-01', 'M'),
...     pd.Period('2024-01-02', 'D'): pd.Period('2024-01', 'M'),
...     pd.Period('2024-07-01', 'D'): pd.Period('2024-07', 'M'),
...     pd.Period('2024-07-02', 'D'): pd.Period('2024-07', 'M'),
... }
>>> parent_to_group = {
...     pd.Period('2024-01', 'M'): 'winter',
...     pd.Period('2024-07', 'M'): 'summer',
... }
>>>
>>> gen = GroupQuotaHierarchicalCombiGen(
...     parent_k=2,
...     slice_to_parent_mapping=slice_to_parent,
...     parent_to_group_mapping=parent_to_group,
...     group_quota={'winter': 1, 'summer': 1}
... )
>>> gen.count(list(slice_to_parent.keys()))  # 1 * 1 = 1
    1
Source code in energy_repset/combi_gens/hierarchical_group_quota.py
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
class GroupQuotaHierarchicalCombiGen(CombinationGenerator):
    """Generate combinations respecting quotas per parent-level group.

    This generator combines hierarchical selection (child slices selected in complete
    parent groups) with group quotas (e.g., exactly 1 month per season). It enables
    high-resolution features (e.g. per-day) while enforcing structural constraints
    at the parent level (e.g. months).

    Args:
        parent_k: Total number of parent groups to select. Must equal sum of group quotas.
        slice_to_parent_mapping: Mapping from each child slice to its parent group.
        parent_to_group_mapping: Mapping from parent ID to its group label (e.g., season).
        group_quota: Required count of parents per group.

    Attributes:
        k: Number of parent groups per combination (same as parent_k for protocol compliance).
        slice_to_parent: Child to parent mapping.
        parent_to_group: Parent to group label mapping.
        group_quota: Required count per group.

    Raises:
        ValueError: If sum of group quotas does not equal parent_k.

    Note:
        Use factory methods `from_slicers()` for automatic parent mapping and
        `from_slicers_with_seasons()` for automatic seasonal grouping.

    Examples:
        Manual construction for seasonal month selection:

        >>> from energy_repset.combi_gens import GroupQuotaHierarchicalCombiGen
        >>> import pandas as pd
        >>>
        >>> slice_to_parent = {
        ...     pd.Period('2024-01-01', 'D'): pd.Period('2024-01', 'M'),
        ...     pd.Period('2024-01-02', 'D'): pd.Period('2024-01', 'M'),
        ...     pd.Period('2024-07-01', 'D'): pd.Period('2024-07', 'M'),
        ...     pd.Period('2024-07-02', 'D'): pd.Period('2024-07', 'M'),
        ... }
        >>> parent_to_group = {
        ...     pd.Period('2024-01', 'M'): 'winter',
        ...     pd.Period('2024-07', 'M'): 'summer',
        ... }
        >>>
        >>> gen = GroupQuotaHierarchicalCombiGen(
        ...     parent_k=2,
        ...     slice_to_parent_mapping=slice_to_parent,
        ...     parent_to_group_mapping=parent_to_group,
        ...     group_quota={'winter': 1, 'summer': 1}
        ... )
        >>> gen.count(list(slice_to_parent.keys()))  # 1 * 1 = 1
            1
    """

    def __init__(
        self,
        parent_k: int,
        slice_to_parent_mapping: Dict[Hashable, Hashable],
        parent_to_group_mapping: Dict[Hashable, Hashable],
        group_quota: Dict[Hashable, int]
    ) -> None:
        """Initialize hierarchical quota generator.

        Args:
            parent_k: Total number of parent groups to select.
            slice_to_parent_mapping: Dict mapping each child slice to its parent.
            parent_to_group_mapping: Mapping from parent ID to group label.
            group_quota: Required count per group.

        Raises:
            ValueError: If sum of group quotas does not equal parent_k, or if any
                parent in slice_to_parent_mapping is missing from parent_to_group_mapping.
        """
        self.k = parent_k  # For CombinationGenerator protocol compliance
        self.slice_to_parent = slice_to_parent_mapping
        self.parent_to_group = parent_to_group_mapping
        self.group_quota = group_quota

        self._validate_configuration(parent_k, slice_to_parent_mapping, parent_to_group_mapping, group_quota)

    @classmethod
    def from_slicers(
        cls,
        parent_k: int,
        dt_index: pd.DatetimeIndex,
        child_slicer: TimeSlicer,
        parent_slicer: TimeSlicer,
        parent_to_group_mapping: Dict[Hashable, Hashable],
        group_quota: Dict[Hashable, int]
    ) -> GroupQuotaHierarchicalCombiGen:
        """Factory method to create generator from slicers with custom group mapping.

        Args:
            parent_k: Total number of parent groups to select.
            dt_index: DatetimeIndex of the time series data.
            child_slicer: TimeSlicer defining child slice granularity (e.g., daily).
            parent_slicer: TimeSlicer defining parent slice granularity (e.g., monthly).
            parent_to_group_mapping: Dict mapping parent IDs to group labels.
            group_quota: Required count per group.

        Returns:
            GroupQuotaHierarchicalCombinationGenerator with auto-constructed child-to-parent mapping.

        Raises:
            ValueError: If quotas invalid.

        Examples:
            Custom grouping of months into seasons:

            >>> import pandas as pd
            >>> from energy_repset.time_slicer import TimeSlicer
            >>> from energy_repset.combi_gens import GroupQuotaHierarchicalCombiGen
            >>>
            >>> dates = pd.date_range('2024-01-01', periods=366, freq='D')
            >>> child_slicer = TimeSlicer(unit='day')
            >>> parent_slicer = TimeSlicer(unit='month')
            >>>
            >>> parent_to_group = {
            ...     pd.Period('2024-01', 'M'): 'winter',
            ...     pd.Period('2024-02', 'M'): 'winter',
            ...     # ... define for all 12 months
            ... }
            >>>
            >>> gen = GroupQuotaHierarchicalCombiGen.from_slicers(
            ...     parent_k=4,
            ...     dt_index=dates,
            ...     child_slicer=child_slicer,
            ...     parent_slicer=parent_slicer,
            ...     parent_to_group_mapping=parent_to_group,
            ...     group_quota={'winter': 1, 'spring': 1, 'summer': 1, 'fall': 1}
            ... )
        """
        child_labels = child_slicer.labels_for_index(dt_index)
        parent_labels = parent_slicer.labels_for_index(dt_index)

        slice_to_parent = {}
        for child, parent in zip(child_labels, parent_labels):
            slice_to_parent[child] = parent

        unique_slice_to_parent = {child: slice_to_parent[child] for child in child_labels.unique()}

        return cls(
            parent_k=parent_k,
            slice_to_parent_mapping=unique_slice_to_parent,
            parent_to_group_mapping=parent_to_group_mapping,
            group_quota=group_quota
        )

    @classmethod
    def from_slicers_with_seasons(
        cls,
        parent_k: int,
        dt_index: pd.DatetimeIndex,
        child_slicer: TimeSlicer,
        group_quota: Dict[Literal['winter', 'spring', 'summer', 'fall'], int]
    ) -> GroupQuotaHierarchicalCombiGen:
        """Factory method with automatic seasonal grouping of months.

        Args:
            parent_k: Total number of parent groups to select (must equal sum of quotas).
            dt_index: DatetimeIndex of the time series data.
            child_slicer: TimeSlicer defining child slice granularity (e.g., daily).
            group_quota: Required count per season. Keys must be subset of
                {'winter', 'spring', 'summer', 'fall'}.

        Returns:
            GroupQuotaHierarchicalCombinationGenerator with seasonal parent grouping.

        Raises:
            ValueError: If quotas invalid.

        Note:
            This factory method uses monthly parents regardless of child slicer.
            Seasons are assigned as: winter (Dec/Jan/Feb), spring (Mar/Apr/May),
            summer (Jun/Jul/Aug), fall (Sep/Oct/Nov).

        Examples:
            Select 4 months (1 per season) from daily data:

            >>> import pandas as pd
            >>> from energy_repset.time_slicer import TimeSlicer
            >>> from energy_repset.combi_gens import GroupQuotaHierarchicalCombiGen
            >>>
            >>> dates = pd.date_range('2024-01-01', periods=366, freq='D')
            >>> child_slicer = TimeSlicer(unit='day')
            >>>
            >>> gen = GroupQuotaHierarchicalCombiGen.from_slicers_with_seasons(
            ...     parent_k=4,
            ...     dt_index=dates,
            ...     child_slicer=child_slicer,
            ...     group_quota={'winter': 1, 'spring': 1, 'summer': 1, 'fall': 1}
            ... )
            >>> gen.count(child_slicer.unique_slices(dates))  # 3 * 3 * 3 * 3 = 81
                81
        """
        child_labels = child_slicer.labels_for_index(dt_index)
        parent_slicer = TimeSlicer(unit='month')
        parent_labels = parent_slicer.labels_for_index(dt_index)

        slice_to_parent = {}
        all_parents = set()
        for child, parent in zip(child_labels, parent_labels):
            slice_to_parent[child] = parent
            all_parents.add(parent)

        unique_slice_to_parent = {child: slice_to_parent[child] for child in child_labels.unique()}
        parent_to_group = cls._assign_seasons(list(all_parents))

        return cls(
            parent_k=parent_k,
            slice_to_parent_mapping=unique_slice_to_parent,
            parent_to_group_mapping=parent_to_group,
            group_quota=group_quota
        )

    @staticmethod
    def _validate_configuration(
        parent_k: int,
        slice_to_parent_mapping: Dict[Hashable, Hashable],
        parent_to_group_mapping: Dict[Hashable, Hashable],
        group_quota: Dict[Hashable, int]
    ) -> None:
        """Validate the configuration of mappings and quotas.

        Args:
            parent_k: Total number of parent groups to select.
            slice_to_parent_mapping: Dict mapping each child slice to its parent.
            parent_to_group_mapping: Mapping from parent ID to group label.
            group_quota: Required count per group.

        Raises:
            ValueError: If sum of group quotas does not equal parent_k, or if any
                parent in slice_to_parent_mapping is missing from parent_to_group_mapping.
        """
        if sum(group_quota.values()) != parent_k:
            raise ValueError(
                f"Sum of group quotas ({sum(group_quota.values())}) must equal parent_k ({parent_k})"
            )

        unique_parents = set(slice_to_parent_mapping.values())
        missing_parents = unique_parents - set(parent_to_group_mapping.keys())
        if missing_parents:
            raise ValueError(
                f"All parents in slice_to_parent_mapping must have a group mapping. "
                f"Missing group mappings for parents: {sorted(missing_parents)}"
            )

    @staticmethod
    def _assign_seasons(months: list[pd.Period]) -> Dict[pd.Period, str]:
        """Assign meteorological seasons to month Period objects.

        Args:
            months: List of monthly Period objects.

        Returns:
            Dict mapping each month to its season: winter (Dec/Jan/Feb),
            spring (Mar/Apr/May), summer (Jun/Jul/Aug), fall (Sep/Oct/Nov).
        """
        season_map = {}
        for month in months:
            m = month.month
            if m in [12, 1, 2]:
                season_map[month] = 'winter'
            elif m in [3, 4, 5]:
                season_map[month] = 'spring'
            elif m in [6, 7, 8]:
                season_map[month] = 'summer'
            else:
                season_map[month] = 'fall'
        return season_map

    def generate(self, unique_slices: Sequence[Hashable]) -> Iterator[SliceCombination]:
        """Generate combinations respecting group quotas, yielding flattened child slices.

        Args:
            unique_slices: Sequence of child slice labels.

        Yields:
            Tuples containing all child slices from parent_k parent groups satisfying quotas.
        """
        parent_to_children: Dict[Hashable, list] = {}
        for child in unique_slices:
            parent = self.slice_to_parent[child]
            parent_to_children.setdefault(parent, []).append(child)

        unique_parents = set(self.slice_to_parent[s] for s in unique_slices)
        groups_of_parents: Dict[Hashable, list] = {}
        for parent in unique_parents:
            group = self.parent_to_group[parent]
            groups_of_parents.setdefault(group, []).append(parent)

        per_group_combis = []
        for group, quota in self.group_quota.items():
            parents_in_group = groups_of_parents.get(group, [])
            per_group_combis.append(list(itertools.combinations(parents_in_group, quota)))

        for parent_combi_tuple in itertools.product(*per_group_combis):
            all_selected_parents = list(itertools.chain.from_iterable(parent_combi_tuple))
            child_slices = []
            for parent in sorted(all_selected_parents):
                child_slices.extend(parent_to_children[parent])
            yield tuple(child_slices)

    def count(self, unique_slices: Sequence[Hashable]) -> int:
        """Count total combinations respecting group quotas.

        Args:
            unique_slices: Sequence of child slice labels.

        Returns:
            Product of C(n_parents_in_group, quota) across all groups.
        """
        unique_parents = set(self.slice_to_parent[s] for s in unique_slices)
        groups_of_parents: Dict[Hashable, list] = {}
        for parent in unique_parents:
            group = self.parent_to_group[parent]
            groups_of_parents.setdefault(group, []).append(parent)

        total = 1
        for group, quota in self.group_quota.items():
            n_parents = len(groups_of_parents.get(group, []))
            total *= math.comb(n_parents, quota)
        return total

    def combination_is_valid(
        self,
        combination: SliceCombination,
        unique_slices: Sequence[Hashable]
    ) -> bool:
        """Check if combination satisfies group quotas and completeness.

        Args:
            combination: Tuple of child slice labels to validate.
            unique_slices: Sequence of all valid child slice labels.

        Returns:
            True if combination contains complete parent groups satisfying quotas.
        """
        if not all(c in unique_slices for c in combination):
            return False

        parent_to_children: Dict[Hashable, set] = {}
        for child in unique_slices:
            parent = self.slice_to_parent[child]
            parent_to_children.setdefault(parent, set()).add(child)

        parents_in_combi = set()
        for child in combination:
            if child not in self.slice_to_parent:
                return False
            parents_in_combi.add(self.slice_to_parent[child])

        if len(parents_in_combi) != self.k:
            return False

        expected_children = set()
        for parent in parents_in_combi:
            expected_children.update(parent_to_children[parent])
        if set(combination) != expected_children:
            return False

        group_count = {group: 0 for group in self.group_quota.keys()}
        for parent in parents_in_combi:
            group = self.parent_to_group[parent]
            group_count[group] += 1

        return all(group_count[g] == self.group_quota[g] for g in self.group_quota.keys())

__init__

__init__(parent_k: int, slice_to_parent_mapping: dict[Hashable, Hashable], parent_to_group_mapping: dict[Hashable, Hashable], group_quota: dict[Hashable, int]) -> None

Initialize hierarchical quota generator.

Parameters:

Name Type Description Default
parent_k int

Total number of parent groups to select.

required
slice_to_parent_mapping dict[Hashable, Hashable]

Dict mapping each child slice to its parent.

required
parent_to_group_mapping dict[Hashable, Hashable]

Mapping from parent ID to group label.

required
group_quota dict[Hashable, int]

Required count per group.

required

Raises:

Type Description
ValueError

If sum of group quotas does not equal parent_k, or if any parent in slice_to_parent_mapping is missing from parent_to_group_mapping.

Source code in energy_repset/combi_gens/hierarchical_group_quota.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def __init__(
    self,
    parent_k: int,
    slice_to_parent_mapping: Dict[Hashable, Hashable],
    parent_to_group_mapping: Dict[Hashable, Hashable],
    group_quota: Dict[Hashable, int]
) -> None:
    """Initialize hierarchical quota generator.

    Args:
        parent_k: Total number of parent groups to select.
        slice_to_parent_mapping: Dict mapping each child slice to its parent.
        parent_to_group_mapping: Mapping from parent ID to group label.
        group_quota: Required count per group.

    Raises:
        ValueError: If sum of group quotas does not equal parent_k, or if any
            parent in slice_to_parent_mapping is missing from parent_to_group_mapping.
    """
    self.k = parent_k  # For CombinationGenerator protocol compliance
    self.slice_to_parent = slice_to_parent_mapping
    self.parent_to_group = parent_to_group_mapping
    self.group_quota = group_quota

    self._validate_configuration(parent_k, slice_to_parent_mapping, parent_to_group_mapping, group_quota)

from_slicers classmethod

from_slicers(parent_k: int, dt_index: DatetimeIndex, child_slicer: TimeSlicer, parent_slicer: TimeSlicer, parent_to_group_mapping: dict[Hashable, Hashable], group_quota: dict[Hashable, int]) -> GroupQuotaHierarchicalCombiGen

Factory method to create generator from slicers with custom group mapping.

Parameters:

Name Type Description Default
parent_k int

Total number of parent groups to select.

required
dt_index DatetimeIndex

DatetimeIndex of the time series data.

required
child_slicer TimeSlicer

TimeSlicer defining child slice granularity (e.g., daily).

required
parent_slicer TimeSlicer

TimeSlicer defining parent slice granularity (e.g., monthly).

required
parent_to_group_mapping dict[Hashable, Hashable]

Dict mapping parent IDs to group labels.

required
group_quota dict[Hashable, int]

Required count per group.

required

Returns:

Type Description
GroupQuotaHierarchicalCombiGen

GroupQuotaHierarchicalCombinationGenerator with auto-constructed child-to-parent mapping.

Raises:

Type Description
ValueError

If quotas invalid.

Examples:

Custom grouping of months into seasons:

>>> import pandas as pd
>>> from energy_repset.time_slicer import TimeSlicer
>>> from energy_repset.combi_gens import GroupQuotaHierarchicalCombiGen
>>>
>>> dates = pd.date_range('2024-01-01', periods=366, freq='D')
>>> child_slicer = TimeSlicer(unit='day')
>>> parent_slicer = TimeSlicer(unit='month')
>>>
>>> parent_to_group = {
...     pd.Period('2024-01', 'M'): 'winter',
...     pd.Period('2024-02', 'M'): 'winter',
...     # ... define for all 12 months
... }
>>>
>>> gen = GroupQuotaHierarchicalCombiGen.from_slicers(
...     parent_k=4,
...     dt_index=dates,
...     child_slicer=child_slicer,
...     parent_slicer=parent_slicer,
...     parent_to_group_mapping=parent_to_group,
...     group_quota={'winter': 1, 'spring': 1, 'summer': 1, 'fall': 1}
... )
Source code in energy_repset/combi_gens/hierarchical_group_quota.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
@classmethod
def from_slicers(
    cls,
    parent_k: int,
    dt_index: pd.DatetimeIndex,
    child_slicer: TimeSlicer,
    parent_slicer: TimeSlicer,
    parent_to_group_mapping: Dict[Hashable, Hashable],
    group_quota: Dict[Hashable, int]
) -> GroupQuotaHierarchicalCombiGen:
    """Factory method to create generator from slicers with custom group mapping.

    Args:
        parent_k: Total number of parent groups to select.
        dt_index: DatetimeIndex of the time series data.
        child_slicer: TimeSlicer defining child slice granularity (e.g., daily).
        parent_slicer: TimeSlicer defining parent slice granularity (e.g., monthly).
        parent_to_group_mapping: Dict mapping parent IDs to group labels.
        group_quota: Required count per group.

    Returns:
        GroupQuotaHierarchicalCombinationGenerator with auto-constructed child-to-parent mapping.

    Raises:
        ValueError: If quotas invalid.

    Examples:
        Custom grouping of months into seasons:

        >>> import pandas as pd
        >>> from energy_repset.time_slicer import TimeSlicer
        >>> from energy_repset.combi_gens import GroupQuotaHierarchicalCombiGen
        >>>
        >>> dates = pd.date_range('2024-01-01', periods=366, freq='D')
        >>> child_slicer = TimeSlicer(unit='day')
        >>> parent_slicer = TimeSlicer(unit='month')
        >>>
        >>> parent_to_group = {
        ...     pd.Period('2024-01', 'M'): 'winter',
        ...     pd.Period('2024-02', 'M'): 'winter',
        ...     # ... define for all 12 months
        ... }
        >>>
        >>> gen = GroupQuotaHierarchicalCombiGen.from_slicers(
        ...     parent_k=4,
        ...     dt_index=dates,
        ...     child_slicer=child_slicer,
        ...     parent_slicer=parent_slicer,
        ...     parent_to_group_mapping=parent_to_group,
        ...     group_quota={'winter': 1, 'spring': 1, 'summer': 1, 'fall': 1}
        ... )
    """
    child_labels = child_slicer.labels_for_index(dt_index)
    parent_labels = parent_slicer.labels_for_index(dt_index)

    slice_to_parent = {}
    for child, parent in zip(child_labels, parent_labels):
        slice_to_parent[child] = parent

    unique_slice_to_parent = {child: slice_to_parent[child] for child in child_labels.unique()}

    return cls(
        parent_k=parent_k,
        slice_to_parent_mapping=unique_slice_to_parent,
        parent_to_group_mapping=parent_to_group_mapping,
        group_quota=group_quota
    )

from_slicers_with_seasons classmethod

from_slicers_with_seasons(parent_k: int, dt_index: DatetimeIndex, child_slicer: TimeSlicer, group_quota: dict[Literal['winter', 'spring', 'summer', 'fall'], int]) -> GroupQuotaHierarchicalCombiGen

Factory method with automatic seasonal grouping of months.

Parameters:

Name Type Description Default
parent_k int

Total number of parent groups to select (must equal sum of quotas).

required
dt_index DatetimeIndex

DatetimeIndex of the time series data.

required
child_slicer TimeSlicer

TimeSlicer defining child slice granularity (e.g., daily).

required
group_quota dict[Literal['winter', 'spring', 'summer', 'fall'], int]

Required count per season. Keys must be subset of {'winter', 'spring', 'summer', 'fall'}.

required

Returns:

Type Description
GroupQuotaHierarchicalCombiGen

GroupQuotaHierarchicalCombinationGenerator with seasonal parent grouping.

Raises:

Type Description
ValueError

If quotas invalid.

Note

This factory method uses monthly parents regardless of child slicer. Seasons are assigned as: winter (Dec/Jan/Feb), spring (Mar/Apr/May), summer (Jun/Jul/Aug), fall (Sep/Oct/Nov).

Examples:

Select 4 months (1 per season) from daily data:

>>> import pandas as pd
>>> from energy_repset.time_slicer import TimeSlicer
>>> from energy_repset.combi_gens import GroupQuotaHierarchicalCombiGen
>>>
>>> dates = pd.date_range('2024-01-01', periods=366, freq='D')
>>> child_slicer = TimeSlicer(unit='day')
>>>
>>> gen = GroupQuotaHierarchicalCombiGen.from_slicers_with_seasons(
...     parent_k=4,
...     dt_index=dates,
...     child_slicer=child_slicer,
...     group_quota={'winter': 1, 'spring': 1, 'summer': 1, 'fall': 1}
... )
>>> gen.count(child_slicer.unique_slices(dates))  # 3 * 3 * 3 * 3 = 81
    81
Source code in energy_repset/combi_gens/hierarchical_group_quota.py
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
@classmethod
def from_slicers_with_seasons(
    cls,
    parent_k: int,
    dt_index: pd.DatetimeIndex,
    child_slicer: TimeSlicer,
    group_quota: Dict[Literal['winter', 'spring', 'summer', 'fall'], int]
) -> GroupQuotaHierarchicalCombiGen:
    """Factory method with automatic seasonal grouping of months.

    Args:
        parent_k: Total number of parent groups to select (must equal sum of quotas).
        dt_index: DatetimeIndex of the time series data.
        child_slicer: TimeSlicer defining child slice granularity (e.g., daily).
        group_quota: Required count per season. Keys must be subset of
            {'winter', 'spring', 'summer', 'fall'}.

    Returns:
        GroupQuotaHierarchicalCombinationGenerator with seasonal parent grouping.

    Raises:
        ValueError: If quotas invalid.

    Note:
        This factory method uses monthly parents regardless of child slicer.
        Seasons are assigned as: winter (Dec/Jan/Feb), spring (Mar/Apr/May),
        summer (Jun/Jul/Aug), fall (Sep/Oct/Nov).

    Examples:
        Select 4 months (1 per season) from daily data:

        >>> import pandas as pd
        >>> from energy_repset.time_slicer import TimeSlicer
        >>> from energy_repset.combi_gens import GroupQuotaHierarchicalCombiGen
        >>>
        >>> dates = pd.date_range('2024-01-01', periods=366, freq='D')
        >>> child_slicer = TimeSlicer(unit='day')
        >>>
        >>> gen = GroupQuotaHierarchicalCombiGen.from_slicers_with_seasons(
        ...     parent_k=4,
        ...     dt_index=dates,
        ...     child_slicer=child_slicer,
        ...     group_quota={'winter': 1, 'spring': 1, 'summer': 1, 'fall': 1}
        ... )
        >>> gen.count(child_slicer.unique_slices(dates))  # 3 * 3 * 3 * 3 = 81
            81
    """
    child_labels = child_slicer.labels_for_index(dt_index)
    parent_slicer = TimeSlicer(unit='month')
    parent_labels = parent_slicer.labels_for_index(dt_index)

    slice_to_parent = {}
    all_parents = set()
    for child, parent in zip(child_labels, parent_labels):
        slice_to_parent[child] = parent
        all_parents.add(parent)

    unique_slice_to_parent = {child: slice_to_parent[child] for child in child_labels.unique()}
    parent_to_group = cls._assign_seasons(list(all_parents))

    return cls(
        parent_k=parent_k,
        slice_to_parent_mapping=unique_slice_to_parent,
        parent_to_group_mapping=parent_to_group,
        group_quota=group_quota
    )

generate

generate(unique_slices: Sequence[Hashable]) -> Iterator[SliceCombination]

Generate combinations respecting group quotas, yielding flattened child slices.

Parameters:

Name Type Description Default
unique_slices Sequence[Hashable]

Sequence of child slice labels.

required

Yields:

Type Description
SliceCombination

Tuples containing all child slices from parent_k parent groups satisfying quotas.

Source code in energy_repset/combi_gens/hierarchical_group_quota.py
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
def generate(self, unique_slices: Sequence[Hashable]) -> Iterator[SliceCombination]:
    """Generate combinations respecting group quotas, yielding flattened child slices.

    Args:
        unique_slices: Sequence of child slice labels.

    Yields:
        Tuples containing all child slices from parent_k parent groups satisfying quotas.
    """
    parent_to_children: Dict[Hashable, list] = {}
    for child in unique_slices:
        parent = self.slice_to_parent[child]
        parent_to_children.setdefault(parent, []).append(child)

    unique_parents = set(self.slice_to_parent[s] for s in unique_slices)
    groups_of_parents: Dict[Hashable, list] = {}
    for parent in unique_parents:
        group = self.parent_to_group[parent]
        groups_of_parents.setdefault(group, []).append(parent)

    per_group_combis = []
    for group, quota in self.group_quota.items():
        parents_in_group = groups_of_parents.get(group, [])
        per_group_combis.append(list(itertools.combinations(parents_in_group, quota)))

    for parent_combi_tuple in itertools.product(*per_group_combis):
        all_selected_parents = list(itertools.chain.from_iterable(parent_combi_tuple))
        child_slices = []
        for parent in sorted(all_selected_parents):
            child_slices.extend(parent_to_children[parent])
        yield tuple(child_slices)

count

count(unique_slices: Sequence[Hashable]) -> int

Count total combinations respecting group quotas.

Parameters:

Name Type Description Default
unique_slices Sequence[Hashable]

Sequence of child slice labels.

required

Returns:

Type Description
int

Product of C(n_parents_in_group, quota) across all groups.

Source code in energy_repset/combi_gens/hierarchical_group_quota.py
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
def count(self, unique_slices: Sequence[Hashable]) -> int:
    """Count total combinations respecting group quotas.

    Args:
        unique_slices: Sequence of child slice labels.

    Returns:
        Product of C(n_parents_in_group, quota) across all groups.
    """
    unique_parents = set(self.slice_to_parent[s] for s in unique_slices)
    groups_of_parents: Dict[Hashable, list] = {}
    for parent in unique_parents:
        group = self.parent_to_group[parent]
        groups_of_parents.setdefault(group, []).append(parent)

    total = 1
    for group, quota in self.group_quota.items():
        n_parents = len(groups_of_parents.get(group, []))
        total *= math.comb(n_parents, quota)
    return total

combination_is_valid

combination_is_valid(combination: SliceCombination, unique_slices: Sequence[Hashable]) -> bool

Check if combination satisfies group quotas and completeness.

Parameters:

Name Type Description Default
combination SliceCombination

Tuple of child slice labels to validate.

required
unique_slices Sequence[Hashable]

Sequence of all valid child slice labels.

required

Returns:

Type Description
bool

True if combination contains complete parent groups satisfying quotas.

Source code in energy_repset/combi_gens/hierarchical_group_quota.py
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
def combination_is_valid(
    self,
    combination: SliceCombination,
    unique_slices: Sequence[Hashable]
) -> bool:
    """Check if combination satisfies group quotas and completeness.

    Args:
        combination: Tuple of child slice labels to validate.
        unique_slices: Sequence of all valid child slice labels.

    Returns:
        True if combination contains complete parent groups satisfying quotas.
    """
    if not all(c in unique_slices for c in combination):
        return False

    parent_to_children: Dict[Hashable, set] = {}
    for child in unique_slices:
        parent = self.slice_to_parent[child]
        parent_to_children.setdefault(parent, set()).add(child)

    parents_in_combi = set()
    for child in combination:
        if child not in self.slice_to_parent:
            return False
        parents_in_combi.add(self.slice_to_parent[child])

    if len(parents_in_combi) != self.k:
        return False

    expected_children = set()
    for parent in parents_in_combi:
        expected_children.update(parent_to_children[parent])
    if set(combination) != expected_children:
        return False

    group_count = {group: 0 for group in self.group_quota.keys()}
    for parent in parents_in_combi:
        group = self.parent_to_group[parent]
        group_count[group] += 1

    return all(group_count[g] == self.group_quota[g] for g in self.group_quota.keys())