Skip to content

Rules

hgp_lib.rules.rules.Rule

Bases: ABC

Abstract base class for logical rule nodes used in rule trees.

Each Rule represents either: - An operator: a logical operation combining subrules (e.g., And, Or), or - A literal: a literal condition (e.g., Literal(value=5, negated=True)).

Rules can be nested to form complex logical expressions. The tree can be traversed, copied, or evaluated against data.

Attributes:

Name Type Description
subrules list[Rule] | None

The list of child rules, for operators, or None, for literals. Default: None.

parent Rule | None

A reference to the parent rule in the tree (if any). Default: None.

value int | None

The value held by this rule (e.g., for literals). Should be None for operators. Default: None.

negated bool

Whether this rule or literal is logically negated (e.g., ~A). Default: False.

copy_subrules bool

Whether to deep copy the subrules (valid only for operators). If False, the subrules are moved. Default: True.

Notes
  • __slots__ are used for performance optimization to reduce memory overhead.
  • No runtime validation is performed for speed; incorrect usage may cause undefined behavior.

Examples:

>>> from hgp_lib.rules import And, Or, Literal
>>> rule = And([
...     Literal(value=0),
...     Or([Literal(value=1, negated=True), Literal(value=2)]),
...     Literal(value=3)
... ])
>>> rule
And(0, Or(~1, 2), 3)
Source code in src/hgp_lib/rules/rules.py
  8
  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
 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
class Rule(ABC):
    """
    Abstract base class for logical rule nodes used in rule trees.

    Each `Rule` represents either:
    - An `operator`: a logical operation combining subrules (e.g., `And`, `Or`), or
    - A `literal`: a literal condition (e.g., `Literal(value=5, negated=True)`).

    Rules can be nested to form complex logical expressions.
    The tree can be traversed, copied, or evaluated against data.

    Attributes:
        subrules (list[Rule] | None):
            The list of child rules, for operators, or `None`, for literals. Default: `None`.
        parent (Rule | None):
            A reference to the parent rule in the tree (if any). Default: `None`.
        value (int | None):
            The value held by this rule (e.g., for literals). Should be `None` for operators. Default: `None`.
        negated (bool):
            Whether this rule or literal is logically negated (e.g., `~A`). Default: `False`.
        copy_subrules (bool):
            Whether to deep copy the subrules (valid only for operators). If False, the subrules are moved.
            Default: `True`.

    Notes:
        - `__slots__` are used for performance optimization to reduce memory overhead.
        - No runtime validation is performed for speed; incorrect usage may cause undefined behavior.

    Examples:
        >>> from hgp_lib.rules import And, Or, Literal
        >>> rule = And([
        ...     Literal(value=0),
        ...     Or([Literal(value=1, negated=True), Literal(value=2)]),
        ...     Literal(value=3)
        ... ])
        >>> rule
        And(0, Or(~1, 2), 3)
    """

    __slots__ = ("negated", "parent", "subrules", "value")

    def __init__(
        self,
        subrules: list["Rule"] | None = None,
        parent: "Rule | None" = None,
        value: int | None = None,
        negated: bool = False,
        copy_subrules: bool = True,
    ):
        if subrules is not None:
            if copy_subrules:
                self.subrules = [s.copy(self) for s in subrules]
            else:
                for s in subrules:
                    s.parent = self
                self.subrules = subrules
        else:
            self.subrules = []

        self.parent = parent
        self.value = value
        self.negated = negated

    def flatten(self):
        """
        Iteratively flattens the rule subtree into a single list of all `Rule` nodes  using a queue.

        Returns:
            list[Rule]: A flat list containing `self` followed by all descendant rules in right-to-left
                preorder sequence.

        Examples:
            >>> from hgp_lib.rules import And, Or, Literal
            >>> rule = And([
            ...     Literal(value=0),
            ...     Or([Literal(value=1, negated=True), Or([Literal(value=2), Literal(value=4)])]),
            ...     Literal(value=3)
            ... ])
            >>> rule.flatten()
            [And(0, Or(~1, Or(2, 4)), 3), 0, Or(~1, Or(2, 4)), 3, ~1, Or(2, 4), 2, 4]
        """
        result = [self]
        queue = [self]
        while queue:
            current = queue.pop()
            result.extend(current.subrules)
            queue.extend(current.subrules)
        return result

    def __len__(self) -> int:
        """
        Returns the total number of nodes in this rule subtree, including the current rule and all its descendants.

        Returns:
            int: The total number of `Rule` nodes in this subtree.

        Examples:
            >>> from hgp_lib.rules import And, Or, Literal
            >>> len(Literal(value=1))
            1
            >>> len(Or([Literal(value=2), Literal(value=3)]))
            3
            >>> len(And([Literal(value=1), Or([Literal(value=2), Literal(value=3)])]))
            5
        """
        result = 0
        queue = [self]
        while queue:
            result += len(queue)
            next_queue = []
            for current in queue:
                if current.subrules:
                    next_queue.extend(current.subrules)
            queue = next_queue
        return result

    def to_str(
        self, feature_names: Sequence[str] | None = None, indent: int = -1
    ) -> str:
        """
        Returns a human-readable string representation of this rule and replaces the literal values with the feature
        names if available.

        Args:
            feature_names (Sequence[str] | None): Output feature names, index-aligned so that
                ``feature_names[value]`` is the name of the feature a literal references.
                Typically obtained from ``Binarizer.get_feature_names_out()``. When provided,
                literal indices are replaced by their names. Default: `None`.
            indent (int): The indentation level when printing the rules. If `-1`, no indentation is used.
                For standard indentation, use `0`. Default: `-1`.

        Returns:
            str: A string representation such as `And(A, B)` or `Literal(1)`.

        Examples:
            >>> from hgp_lib.rules import And, Literal
            >>> str(And([Literal(value=1), Literal(value=2)]))
            'And(1, 2)'
            >>> str(And([Literal(value=1), Literal(value=2)], negated=True))
            '~And(1, 2)'
            >>> And([Literal(value=1), Literal(value=2)], negated=True).to_str(["a", "good", "nice"])
            '~And(good, nice)'
        """
        if indent == -1:
            rez = f"{type(self).__name__}({', '.join(s.to_str(feature_names, indent) for s in self.subrules)})"
        else:
            new_indent = indent + 1
            separator = ",\n"
            tab = "\t"
            to_join = separator.join(
                tab * new_indent + s.to_str(feature_names, new_indent)
                for s in self.subrules
            )
            rez = f"{type(self).__name__}(\n{to_join}\n{tab * indent})"

        if self.negated:
            return "~" + rez
        return rez

    def __str__(self) -> str:
        return self.to_str()

    def __repr__(self) -> str:
        return self.to_str()

    def copy(self, parent: "Rule | None" = None) -> "Rule":
        """
        Creates a deep copy of this rule and its entire subtree, optionally assigning a new parent.

        Args:
            parent (Rule | None):
                The parent rule for the new copy. If omitted, retains the current parent. Default: `None`.

        Returns:
            Rule: A new instance of the same rule type, with all subrules recursively copied.

        Examples:
            >>> from hgp_lib.rules import And, Literal
            >>> a = And([Literal(value=1), Literal(value=2)])
            >>> b = a.copy()
            >>> a is b
            False
            >>> a.subrules[0] is b.subrules[0]
            False
            >>> all([(x.value is None and y.value is None) or (x.value == y.value) for x, y in zip(a.flatten(), b.flatten())])
            True
        """
        new = object.__new__(type(self))
        new.subrules = [s.copy(new) for s in self.subrules]
        new.parent = self.parent if parent is None else parent
        new.value = self.value
        new.negated = self.negated
        return new

    def detach_subtree(self) -> "Rule":
        """
        Creates a shallow copy of this rule and its entire subtree, without referencing the parent of the current rule.
        Can be used for a fast swap between two subtrees without resorting to deep copy.

        Returns:
            Rule: A new instance of the same rule type, keeping the same subrules as the original rule.

        Examples:
            >>> from hgp_lib.rules import And, Literal
            >>> a = And([Literal(value=1), Literal(value=2)])
            >>> b = a.detach_subtree()
            >>> a is b
            False
            >>> a.subrules[0] is b.subrules[0]
            True
            >>> all([(x.value is None and y.value is None) or (x.value == y.value) for x, y in zip(a.flatten(), b.flatten())])
            True
        """
        new = object.__new__(type(self))
        new.subrules = self.subrules
        new.parent = None
        new.value = self.value
        new.negated = self.negated
        return new

    @abstractmethod
    def evaluate(self, data: np.ndarray) -> np.ndarray:
        """
        Abstract method to evaluate this rule against the given data, in a vectorized manner.

        Args:
            data (np.ndarray): The input data. Must be a 2D ndarray, with instances on rows and features on columns.

        Returns:
            np.ndarray:
                The boolean result of evaluating this rule vectorized across all instances.

        Notes:
            Concrete subclasses (`And`, `Or`, `Literal`, etc.) must implement this.
        """

    def apply_feature_mapping(self, feature_mapping: dict[int, int]):
        """
        Applies a feature mapping to this rule and all its subrules in-place.

        This method remaps feature indices used in literals according to the provided
        mapping dictionary. It is used in hierarchical GP when child populations operate
        on a subset of features (feature bagging) and need to be translated back to the
        parent's feature space during crossover.

        Args:
            feature_mapping (dict[int, int]):
                A dictionary mapping old feature indices to new feature indices.
                For literals, `self.value` is replaced with `feature_mapping[self.value]`.

        Returns:
            None: This method modifies the rule in-place.

        Raises:
            KeyError: If `self.value` is not found in `feature_mapping`.

        Examples:
            >>> from hgp_lib.rules import And, Literal
            >>> rule = And([Literal(value=0), Literal(value=1)])
            >>> rule.apply_feature_mapping({0: 5, 1: 10})
            >>> str(rule)
            'And(5, 10)'
        """
        if self.value is not None:
            self.value = feature_mapping[self.value]
        else:
            for subrule in self.subrules:
                subrule.apply_feature_mapping(feature_mapping)

flatten()

Iteratively flattens the rule subtree into a single list of all Rule nodes using a queue.

Returns:

Type Description

list[Rule]: A flat list containing self followed by all descendant rules in right-to-left preorder sequence.

Examples:

>>> from hgp_lib.rules import And, Or, Literal
>>> rule = And([
...     Literal(value=0),
...     Or([Literal(value=1, negated=True), Or([Literal(value=2), Literal(value=4)])]),
...     Literal(value=3)
... ])
>>> rule.flatten()
[And(0, Or(~1, Or(2, 4)), 3), 0, Or(~1, Or(2, 4)), 3, ~1, Or(2, 4), 2, 4]
Source code in src/hgp_lib/rules/rules.py
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
def flatten(self):
    """
    Iteratively flattens the rule subtree into a single list of all `Rule` nodes  using a queue.

    Returns:
        list[Rule]: A flat list containing `self` followed by all descendant rules in right-to-left
            preorder sequence.

    Examples:
        >>> from hgp_lib.rules import And, Or, Literal
        >>> rule = And([
        ...     Literal(value=0),
        ...     Or([Literal(value=1, negated=True), Or([Literal(value=2), Literal(value=4)])]),
        ...     Literal(value=3)
        ... ])
        >>> rule.flatten()
        [And(0, Or(~1, Or(2, 4)), 3), 0, Or(~1, Or(2, 4)), 3, ~1, Or(2, 4), 2, 4]
    """
    result = [self]
    queue = [self]
    while queue:
        current = queue.pop()
        result.extend(current.subrules)
        queue.extend(current.subrules)
    return result

__len__()

Returns the total number of nodes in this rule subtree, including the current rule and all its descendants.

Returns:

Name Type Description
int int

The total number of Rule nodes in this subtree.

Examples:

>>> from hgp_lib.rules import And, Or, Literal
>>> len(Literal(value=1))
1
>>> len(Or([Literal(value=2), Literal(value=3)]))
3
>>> len(And([Literal(value=1), Or([Literal(value=2), Literal(value=3)])]))
5
Source code in src/hgp_lib/rules/rules.py
 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
def __len__(self) -> int:
    """
    Returns the total number of nodes in this rule subtree, including the current rule and all its descendants.

    Returns:
        int: The total number of `Rule` nodes in this subtree.

    Examples:
        >>> from hgp_lib.rules import And, Or, Literal
        >>> len(Literal(value=1))
        1
        >>> len(Or([Literal(value=2), Literal(value=3)]))
        3
        >>> len(And([Literal(value=1), Or([Literal(value=2), Literal(value=3)])]))
        5
    """
    result = 0
    queue = [self]
    while queue:
        result += len(queue)
        next_queue = []
        for current in queue:
            if current.subrules:
                next_queue.extend(current.subrules)
        queue = next_queue
    return result

to_str(feature_names=None, indent=-1)

Returns a human-readable string representation of this rule and replaces the literal values with the feature names if available.

Parameters:

Name Type Description Default
feature_names Sequence[str] | None

Output feature names, index-aligned so that feature_names[value] is the name of the feature a literal references. Typically obtained from Binarizer.get_feature_names_out(). When provided, literal indices are replaced by their names. Default: None.

None
indent int

The indentation level when printing the rules. If -1, no indentation is used. For standard indentation, use 0. Default: -1.

-1

Returns:

Name Type Description
str str

A string representation such as And(A, B) or Literal(1).

Examples:

>>> from hgp_lib.rules import And, Literal
>>> str(And([Literal(value=1), Literal(value=2)]))
'And(1, 2)'
>>> str(And([Literal(value=1), Literal(value=2)], negated=True))
'~And(1, 2)'
>>> And([Literal(value=1), Literal(value=2)], negated=True).to_str(["a", "good", "nice"])
'~And(good, nice)'
Source code in src/hgp_lib/rules/rules.py
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
def to_str(
    self, feature_names: Sequence[str] | None = None, indent: int = -1
) -> str:
    """
    Returns a human-readable string representation of this rule and replaces the literal values with the feature
    names if available.

    Args:
        feature_names (Sequence[str] | None): Output feature names, index-aligned so that
            ``feature_names[value]`` is the name of the feature a literal references.
            Typically obtained from ``Binarizer.get_feature_names_out()``. When provided,
            literal indices are replaced by their names. Default: `None`.
        indent (int): The indentation level when printing the rules. If `-1`, no indentation is used.
            For standard indentation, use `0`. Default: `-1`.

    Returns:
        str: A string representation such as `And(A, B)` or `Literal(1)`.

    Examples:
        >>> from hgp_lib.rules import And, Literal
        >>> str(And([Literal(value=1), Literal(value=2)]))
        'And(1, 2)'
        >>> str(And([Literal(value=1), Literal(value=2)], negated=True))
        '~And(1, 2)'
        >>> And([Literal(value=1), Literal(value=2)], negated=True).to_str(["a", "good", "nice"])
        '~And(good, nice)'
    """
    if indent == -1:
        rez = f"{type(self).__name__}({', '.join(s.to_str(feature_names, indent) for s in self.subrules)})"
    else:
        new_indent = indent + 1
        separator = ",\n"
        tab = "\t"
        to_join = separator.join(
            tab * new_indent + s.to_str(feature_names, new_indent)
            for s in self.subrules
        )
        rez = f"{type(self).__name__}(\n{to_join}\n{tab * indent})"

    if self.negated:
        return "~" + rez
    return rez

copy(parent=None)

Creates a deep copy of this rule and its entire subtree, optionally assigning a new parent.

Parameters:

Name Type Description Default
parent Rule | None

The parent rule for the new copy. If omitted, retains the current parent. Default: None.

None

Returns:

Name Type Description
Rule Rule

A new instance of the same rule type, with all subrules recursively copied.

Examples:

>>> from hgp_lib.rules import And, Literal
>>> a = And([Literal(value=1), Literal(value=2)])
>>> b = a.copy()
>>> a is b
False
>>> a.subrules[0] is b.subrules[0]
False
>>> all([(x.value is None and y.value is None) or (x.value == y.value) for x, y in zip(a.flatten(), b.flatten())])
True
Source code in src/hgp_lib/rules/rules.py
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
def copy(self, parent: "Rule | None" = None) -> "Rule":
    """
    Creates a deep copy of this rule and its entire subtree, optionally assigning a new parent.

    Args:
        parent (Rule | None):
            The parent rule for the new copy. If omitted, retains the current parent. Default: `None`.

    Returns:
        Rule: A new instance of the same rule type, with all subrules recursively copied.

    Examples:
        >>> from hgp_lib.rules import And, Literal
        >>> a = And([Literal(value=1), Literal(value=2)])
        >>> b = a.copy()
        >>> a is b
        False
        >>> a.subrules[0] is b.subrules[0]
        False
        >>> all([(x.value is None and y.value is None) or (x.value == y.value) for x, y in zip(a.flatten(), b.flatten())])
        True
    """
    new = object.__new__(type(self))
    new.subrules = [s.copy(new) for s in self.subrules]
    new.parent = self.parent if parent is None else parent
    new.value = self.value
    new.negated = self.negated
    return new

detach_subtree()

Creates a shallow copy of this rule and its entire subtree, without referencing the parent of the current rule. Can be used for a fast swap between two subtrees without resorting to deep copy.

Returns:

Name Type Description
Rule Rule

A new instance of the same rule type, keeping the same subrules as the original rule.

Examples:

>>> from hgp_lib.rules import And, Literal
>>> a = And([Literal(value=1), Literal(value=2)])
>>> b = a.detach_subtree()
>>> a is b
False
>>> a.subrules[0] is b.subrules[0]
True
>>> all([(x.value is None and y.value is None) or (x.value == y.value) for x, y in zip(a.flatten(), b.flatten())])
True
Source code in src/hgp_lib/rules/rules.py
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
def detach_subtree(self) -> "Rule":
    """
    Creates a shallow copy of this rule and its entire subtree, without referencing the parent of the current rule.
    Can be used for a fast swap between two subtrees without resorting to deep copy.

    Returns:
        Rule: A new instance of the same rule type, keeping the same subrules as the original rule.

    Examples:
        >>> from hgp_lib.rules import And, Literal
        >>> a = And([Literal(value=1), Literal(value=2)])
        >>> b = a.detach_subtree()
        >>> a is b
        False
        >>> a.subrules[0] is b.subrules[0]
        True
        >>> all([(x.value is None and y.value is None) or (x.value == y.value) for x, y in zip(a.flatten(), b.flatten())])
        True
    """
    new = object.__new__(type(self))
    new.subrules = self.subrules
    new.parent = None
    new.value = self.value
    new.negated = self.negated
    return new

evaluate(data) abstractmethod

Abstract method to evaluate this rule against the given data, in a vectorized manner.

Parameters:

Name Type Description Default
data ndarray

The input data. Must be a 2D ndarray, with instances on rows and features on columns.

required

Returns:

Type Description
ndarray

np.ndarray: The boolean result of evaluating this rule vectorized across all instances.

Notes

Concrete subclasses (And, Or, Literal, etc.) must implement this.

Source code in src/hgp_lib/rules/rules.py
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
@abstractmethod
def evaluate(self, data: np.ndarray) -> np.ndarray:
    """
    Abstract method to evaluate this rule against the given data, in a vectorized manner.

    Args:
        data (np.ndarray): The input data. Must be a 2D ndarray, with instances on rows and features on columns.

    Returns:
        np.ndarray:
            The boolean result of evaluating this rule vectorized across all instances.

    Notes:
        Concrete subclasses (`And`, `Or`, `Literal`, etc.) must implement this.
    """

apply_feature_mapping(feature_mapping)

Applies a feature mapping to this rule and all its subrules in-place.

This method remaps feature indices used in literals according to the provided mapping dictionary. It is used in hierarchical GP when child populations operate on a subset of features (feature bagging) and need to be translated back to the parent's feature space during crossover.

Parameters:

Name Type Description Default
feature_mapping dict[int, int]

A dictionary mapping old feature indices to new feature indices. For literals, self.value is replaced with feature_mapping[self.value].

required

Returns:

Name Type Description
None

This method modifies the rule in-place.

Raises:

Type Description
KeyError

If self.value is not found in feature_mapping.

Examples:

>>> from hgp_lib.rules import And, Literal
>>> rule = And([Literal(value=0), Literal(value=1)])
>>> rule.apply_feature_mapping({0: 5, 1: 10})
>>> str(rule)
'And(5, 10)'
Source code in src/hgp_lib/rules/rules.py
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
def apply_feature_mapping(self, feature_mapping: dict[int, int]):
    """
    Applies a feature mapping to this rule and all its subrules in-place.

    This method remaps feature indices used in literals according to the provided
    mapping dictionary. It is used in hierarchical GP when child populations operate
    on a subset of features (feature bagging) and need to be translated back to the
    parent's feature space during crossover.

    Args:
        feature_mapping (dict[int, int]):
            A dictionary mapping old feature indices to new feature indices.
            For literals, `self.value` is replaced with `feature_mapping[self.value]`.

    Returns:
        None: This method modifies the rule in-place.

    Raises:
        KeyError: If `self.value` is not found in `feature_mapping`.

    Examples:
        >>> from hgp_lib.rules import And, Literal
        >>> rule = And([Literal(value=0), Literal(value=1)])
        >>> rule.apply_feature_mapping({0: 5, 1: 10})
        >>> str(rule)
        'And(5, 10)'
    """
    if self.value is not None:
        self.value = feature_mapping[self.value]
    else:
        for subrule in self.subrules:
            subrule.apply_feature_mapping(feature_mapping)

hgp_lib.rules.literals.Literal

Bases: Rule

Represents a single literal condition in a rule tree.

A Literal corresponds to a single feature (column) in the input data. It may optionally be negated, in which case its logical value is inverted.

Attributes:

Name Type Description
subrules list[Rule] | None

The list of child rules, for operators, or None, for literals. Default: None.

parent Rule | None

A reference to the parent rule in the tree (if any). Default: None.

value int

The column index of the feature this literal refers to in data.

negated bool

Whether the literal is negated (logical NOT).

Examples:

>>> import numpy as np
>>> data = np.array([[True, False, True], [False, True, False]])
>>> literal = Literal(value=0)
>>> literal.evaluate(data)
array([ True, False])
>>> negated_literal = Literal(value=1, negated=True)
>>> negated_literal.evaluate(data)
array([ True, False])
>>> str(Literal(value=2))
'2'
>>> str(Literal(value=2, negated=True))
'~2'
Source code in src/hgp_lib/rules/literals.py
  6
  7
  8
  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
 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
class Literal(Rule):
    """
    Represents a single literal condition in a rule tree.

    A `Literal` corresponds to a single feature (column) in the input `data`.
    It may optionally be negated, in which case its logical value is inverted.

    Attributes:
        subrules (list[Rule] | None):
            The list of child rules, for operators, or `None`, for literals. Default: `None`.
        parent (Rule | None):
            A reference to the parent rule in the tree (if any). Default: `None`.
        value (int):
            The column index of the feature this literal refers to in `data`.
        negated (bool):
            Whether the literal is negated (logical NOT).

    Examples:
        >>> import numpy as np
        >>> data = np.array([[True, False, True], [False, True, False]])
        >>> literal = Literal(value=0)
        >>> literal.evaluate(data)
        array([ True, False])
        >>> negated_literal = Literal(value=1, negated=True)
        >>> negated_literal.evaluate(data)
        array([ True, False])
        >>> str(Literal(value=2))
        '2'
        >>> str(Literal(value=2, negated=True))
        '~2'
    """

    def evaluate(self, data):
        """
        Evaluates this literal on the given data array, based on the `self.value` feature.

        Args:
            data (np.ndarray):
                Input data passed to subrules. Must be a 2D ndarray, with instances on rows and features on columns.
                Not checked at runtime for performance reasons.

        Returns:
            np.ndarray:
                The boolean result of evaluating this rule vectorized across all instances.

        Examples:
            >>> import numpy as np
            >>> data = np.array([[True, False], [False, True]])
            >>> Literal(value=0).evaluate(data)
            array([ True, False])
            >>> Literal(value=1, negated=True).evaluate(data)
            array([ True, False])
        """
        return ~data[:, self.value] if self.negated else data[:, self.value]

    def to_str(
        self, feature_names: Sequence[str] | None = None, indent: int = -1
    ) -> str:
        """
        Returns a human-readable string representation of the literal. The literal can be replaced with the feature
        name if provided.

        Args:
            feature_names (Sequence[str] | None): Output feature names, index-aligned so that
                ``feature_names[value]`` is the name of the feature this literal references.
                When provided, the literal index is replaced by its name. Default: `None`.
            indent (int): Not used. Default: `-1`.

        Returns:
            str: The literal as a string, prefixed with `~` if negated.

        Examples:
            >>> str(Literal(value=0))
            '0'
            >>> str(Literal(value=0, negated=True))
            '~0'
            >>> Literal(value=0, negated=True).to_str(["bad"])
            '~bad'
        """
        value = self.value
        if feature_names is not None:
            value = feature_names[value]
        return f"~{value}" if self.negated else f"{value}"

    def copy(self, parent: "Rule | None" = None) -> "Rule":
        """
        Creates a copy of this Literal, optionally assigning a new parent.
        This uses a faster execution path to create literals.

        Args:
            parent (Rule | None):
                The parent rule for the new copy. If omitted, retains the current parent. Default: `None`.

        Returns:
            Rule: A copy of the current Literal.

        Examples:
            >>> from hgp_lib.rules import And, Literal
            >>> a = Literal(value=1)
            >>> b = a.copy()
            >>> a is b
            False
        """
        new = object.__new__(Literal)
        new.subrules = []
        new.parent = self.parent if parent is None else parent
        new.value = self.value
        new.negated = self.negated
        return new

evaluate(data)

Evaluates this literal on the given data array, based on the self.value feature.

Parameters:

Name Type Description Default
data ndarray

Input data passed to subrules. Must be a 2D ndarray, with instances on rows and features on columns. Not checked at runtime for performance reasons.

required

Returns:

Type Description

np.ndarray: The boolean result of evaluating this rule vectorized across all instances.

Examples:

>>> import numpy as np
>>> data = np.array([[True, False], [False, True]])
>>> Literal(value=0).evaluate(data)
array([ True, False])
>>> Literal(value=1, negated=True).evaluate(data)
array([ True, False])
Source code in src/hgp_lib/rules/literals.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
def evaluate(self, data):
    """
    Evaluates this literal on the given data array, based on the `self.value` feature.

    Args:
        data (np.ndarray):
            Input data passed to subrules. Must be a 2D ndarray, with instances on rows and features on columns.
            Not checked at runtime for performance reasons.

    Returns:
        np.ndarray:
            The boolean result of evaluating this rule vectorized across all instances.

    Examples:
        >>> import numpy as np
        >>> data = np.array([[True, False], [False, True]])
        >>> Literal(value=0).evaluate(data)
        array([ True, False])
        >>> Literal(value=1, negated=True).evaluate(data)
        array([ True, False])
    """
    return ~data[:, self.value] if self.negated else data[:, self.value]

to_str(feature_names=None, indent=-1)

Returns a human-readable string representation of the literal. The literal can be replaced with the feature name if provided.

Parameters:

Name Type Description Default
feature_names Sequence[str] | None

Output feature names, index-aligned so that feature_names[value] is the name of the feature this literal references. When provided, the literal index is replaced by its name. Default: None.

None
indent int

Not used. Default: -1.

-1

Returns:

Name Type Description
str str

The literal as a string, prefixed with ~ if negated.

Examples:

>>> str(Literal(value=0))
'0'
>>> str(Literal(value=0, negated=True))
'~0'
>>> Literal(value=0, negated=True).to_str(["bad"])
'~bad'
Source code in src/hgp_lib/rules/literals.py
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
def to_str(
    self, feature_names: Sequence[str] | None = None, indent: int = -1
) -> str:
    """
    Returns a human-readable string representation of the literal. The literal can be replaced with the feature
    name if provided.

    Args:
        feature_names (Sequence[str] | None): Output feature names, index-aligned so that
            ``feature_names[value]`` is the name of the feature this literal references.
            When provided, the literal index is replaced by its name. Default: `None`.
        indent (int): Not used. Default: `-1`.

    Returns:
        str: The literal as a string, prefixed with `~` if negated.

    Examples:
        >>> str(Literal(value=0))
        '0'
        >>> str(Literal(value=0, negated=True))
        '~0'
        >>> Literal(value=0, negated=True).to_str(["bad"])
        '~bad'
    """
    value = self.value
    if feature_names is not None:
        value = feature_names[value]
    return f"~{value}" if self.negated else f"{value}"

copy(parent=None)

Creates a copy of this Literal, optionally assigning a new parent. This uses a faster execution path to create literals.

Parameters:

Name Type Description Default
parent Rule | None

The parent rule for the new copy. If omitted, retains the current parent. Default: None.

None

Returns:

Name Type Description
Rule Rule

A copy of the current Literal.

Examples:

>>> from hgp_lib.rules import And, Literal
>>> a = Literal(value=1)
>>> b = a.copy()
>>> a is b
False
Source code in src/hgp_lib/rules/literals.py
 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
def copy(self, parent: "Rule | None" = None) -> "Rule":
    """
    Creates a copy of this Literal, optionally assigning a new parent.
    This uses a faster execution path to create literals.

    Args:
        parent (Rule | None):
            The parent rule for the new copy. If omitted, retains the current parent. Default: `None`.

    Returns:
        Rule: A copy of the current Literal.

    Examples:
        >>> from hgp_lib.rules import And, Literal
        >>> a = Literal(value=1)
        >>> b = a.copy()
        >>> a is b
        False
    """
    new = object.__new__(Literal)
    new.subrules = []
    new.parent = self.parent if parent is None else parent
    new.value = self.value
    new.negated = self.negated
    return new

hgp_lib.rules.operators.And

Bases: Rule

Logical conjunction (AND) operator node for rule trees. It evaluates to True only if every subrule evaluates to True.

Attributes:

Name Type Description
subrules list[Rule]

A list of child rules combined with logical AND. Must be a list longer than 1 element. Not checked at runtime for performance reasons. Default: None.

parent Rule | None

A reference to the parent rule, if part of a larger tree. Default: None.

value None

Always None for operator nodes (non-literals). Not checked at runtime for performance reasons. Default: None.

negated bool

Whether the entire conjunction is logically negated (~And(...)). Default: False.

Examples:

>>> from hgp_lib.rules.operators import And, Or
>>> from hgp_lib.rules import Literal
>>> rule = And([
...     Literal(value=0),
...     Or([Literal(value=1, negated=True), Literal(value=2)]),
...     Literal(value=3)
... ])
>>> rule
And(0, Or(~1, 2), 3)
Source code in src/hgp_lib/rules/operators.py
 6
 7
 8
 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
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
class And(Rule):
    """
    Logical conjunction (`AND`) operator node for rule trees. It evaluates to `True` only if every subrule evaluates to
    `True`.

    Attributes:
        subrules (list[Rule]):
            A list of child rules combined with logical AND. Must be a list longer than 1 element. Not checked at
            runtime for performance reasons. Default: `None`.
        parent (Rule | None):
            A reference to the parent rule, if part of a larger tree. Default: `None`.
        value (None):
            Always `None` for operator nodes (non-literals). Not checked at runtime for performance reasons. Default:
            `None`.
        negated (bool):
            Whether the entire conjunction is logically negated (`~And(...)`). Default: `False`.

    Examples:
        >>> from hgp_lib.rules.operators import And, Or
        >>> from hgp_lib.rules import Literal
        >>> rule = And([
        ...     Literal(value=0),
        ...     Or([Literal(value=1, negated=True), Literal(value=2)]),
        ...     Literal(value=3)
        ... ])
        >>> rule
        And(0, Or(~1, 2), 3)
    """

    def evaluate(self, data: np.ndarray) -> np.ndarray:
        """
        All subrules are recursively evaluated, and their results are combined using logical conjunction. The final
        result is optionally negated if `self.negated` is `True`.

        Args:
            data (np.ndarray):
                Input data passed to subrules. Must be a 2D ndarray, with instances on rows and features on columns.
                Not checked at runtime for performance reasons.

        Returns:
            np.ndarray:
                The boolean result of evaluating this rule vectorized across all instances.

        Examples:
            >>> from hgp_lib.rules.operators import And
            >>> from hgp_lib.rules import Literal
            >>> import numpy as np
            >>> data = np.array([
            ...     [True, False],
            ...     [False, False],
            ...     [False, False]
            ... ])
            >>> rule = And([Literal(value=0), Literal(value=1, negated=True)])
            >>> rule.evaluate(data)
            array([ True, False, False])

        Notes:
            This implementation does as few operations as possible, at the expense of more memory usage.
        """
        cols = []
        neg_mask = []
        sub_operators = []
        for s in self.subrules:
            if s.value is not None:  # We have a literal
                cols.append(s.value)
                neg_mask.append(s.negated)
            else:  # We have an operator
                sub_operators.append(s)

        if cols:  # Hot branch for literals
            # One-liner for all literals
            mask = (data[:, cols] ^ np.array(neg_mask)).all(1)
            # Updating with operators
            for s in sub_operators:
                mask &= s.evaluate(data)
        else:  # Hot branch for no literals
            mask = sub_operators[0].evaluate(data)  # Create an initial matrix
            for s in sub_operators[1:]:  # Updating with the rest of the operators
                mask &= s.evaluate(data)

        if self.negated:
            mask = np.logical_not(mask, out=mask)
        return mask

evaluate(data)

All subrules are recursively evaluated, and their results are combined using logical conjunction. The final result is optionally negated if self.negated is True.

Parameters:

Name Type Description Default
data ndarray

Input data passed to subrules. Must be a 2D ndarray, with instances on rows and features on columns. Not checked at runtime for performance reasons.

required

Returns:

Type Description
ndarray

np.ndarray: The boolean result of evaluating this rule vectorized across all instances.

Examples:

>>> from hgp_lib.rules.operators import And
>>> from hgp_lib.rules import Literal
>>> import numpy as np
>>> data = np.array([
...     [True, False],
...     [False, False],
...     [False, False]
... ])
>>> rule = And([Literal(value=0), Literal(value=1, negated=True)])
>>> rule.evaluate(data)
array([ True, False, False])
Notes

This implementation does as few operations as possible, at the expense of more memory usage.

Source code in src/hgp_lib/rules/operators.py
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
def evaluate(self, data: np.ndarray) -> np.ndarray:
    """
    All subrules are recursively evaluated, and their results are combined using logical conjunction. The final
    result is optionally negated if `self.negated` is `True`.

    Args:
        data (np.ndarray):
            Input data passed to subrules. Must be a 2D ndarray, with instances on rows and features on columns.
            Not checked at runtime for performance reasons.

    Returns:
        np.ndarray:
            The boolean result of evaluating this rule vectorized across all instances.

    Examples:
        >>> from hgp_lib.rules.operators import And
        >>> from hgp_lib.rules import Literal
        >>> import numpy as np
        >>> data = np.array([
        ...     [True, False],
        ...     [False, False],
        ...     [False, False]
        ... ])
        >>> rule = And([Literal(value=0), Literal(value=1, negated=True)])
        >>> rule.evaluate(data)
        array([ True, False, False])

    Notes:
        This implementation does as few operations as possible, at the expense of more memory usage.
    """
    cols = []
    neg_mask = []
    sub_operators = []
    for s in self.subrules:
        if s.value is not None:  # We have a literal
            cols.append(s.value)
            neg_mask.append(s.negated)
        else:  # We have an operator
            sub_operators.append(s)

    if cols:  # Hot branch for literals
        # One-liner for all literals
        mask = (data[:, cols] ^ np.array(neg_mask)).all(1)
        # Updating with operators
        for s in sub_operators:
            mask &= s.evaluate(data)
    else:  # Hot branch for no literals
        mask = sub_operators[0].evaluate(data)  # Create an initial matrix
        for s in sub_operators[1:]:  # Updating with the rest of the operators
            mask &= s.evaluate(data)

    if self.negated:
        mask = np.logical_not(mask, out=mask)
    return mask

hgp_lib.rules.operators.Or

Bases: Rule

Logical disjunction (OR) operator node for rule trees. It evaluates to True if any subrule evaluates to True.

Attributes:

Name Type Description
subrules list[Rule]

A list of child rules combined with logical AND. Must be a list longer than 1 element. Not checked at runtime for performance reasons. Default: None.

parent Rule | None

A reference to the parent rule, if part of a larger tree. Default: None.

value None

Always None for operator nodes (non-literals). Not checked at runtime for performance reasons. Default: None.

negated bool

Whether the entire conjunction is logically negated (~And(...)). Default: False.

Examples:

>>> from hgp_lib.rules.operators import And
>>> from hgp_lib.rules import Literal
>>> rule = Or([Literal(value=0), Literal(value=1, negated=True)])
>>> rule
Or(0, ~1)
Source code in src/hgp_lib/rules/operators.py
 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 Or(Rule):
    """
    Logical disjunction (`OR`) operator node for rule trees. It evaluates to `True` if any subrule evaluates to `True`.

    Attributes:
        subrules (list[Rule]):
            A list of child rules combined with logical AND. Must be a list longer than 1 element. Not checked at
            runtime for performance reasons. Default: `None`.
        parent (Rule | None):
            A reference to the parent rule, if part of a larger tree. Default: `None`.
        value (None):
            Always `None` for operator nodes (non-literals). Not checked at runtime for performance reasons. Default:
            `None`.
        negated (bool):
            Whether the entire conjunction is logically negated (`~And(...)`). Default: `False`.

    Examples:
        >>> from hgp_lib.rules.operators import And
        >>> from hgp_lib.rules import Literal
        >>> rule = Or([Literal(value=0), Literal(value=1, negated=True)])
        >>> rule
        Or(0, ~1)
    """

    def evaluate(self, data: np.ndarray) -> np.ndarray:
        """
        All subrules are recursively evaluated, and their results are combined using logical disjunction. The final
        result is optionally negated if `self.negated` is `True`.

        Args:
            data (np.ndarray):
                Input data passed to subrules. Must be a 2D ndarray, with instances on rows and features on columns.
                Not checked at runtime for performance reasons.

        Returns:
            np.ndarray:
                The boolean result of evaluating this rule vectorized across all instances.

        Examples:
            >>> from hgp_lib.rules.operators import And
            >>> from hgp_lib.rules import Literal
            >>> import numpy as np
            >>> data = np.array([
            ...     [True, False, True],
            ...     [False, False, True]
            ... ])
            >>> rule = Or([Literal(value=0), Literal(value=1)])
            >>> rule.evaluate(data)
            array([ True, False])

        Notes:
            This implementation does as few operations as possible, at the expense of more memory usage.
        """
        cols = []
        neg_mask = []
        sub_operators = []
        for s in self.subrules:
            if s.value is not None:  # We have a literal
                cols.append(s.value)
                neg_mask.append(s.negated)
            else:  # We have an operator
                sub_operators.append(s)

        if cols:  # Hot branch for literals
            # One-liner for all literals
            mask = (data[:, cols] ^ np.array(neg_mask)).any(1)
            # Updating with operators
            for s in sub_operators:
                mask |= s.evaluate(data)
        else:  # Hot branch for no literals
            mask = sub_operators[0].evaluate(data)  # Create an initial matrix
            for s in sub_operators[1:]:  # Updating with the rest of the operators
                mask |= s.evaluate(data)

        if self.negated:
            mask = np.logical_not(mask, out=mask)
        return mask

evaluate(data)

All subrules are recursively evaluated, and their results are combined using logical disjunction. The final result is optionally negated if self.negated is True.

Parameters:

Name Type Description Default
data ndarray

Input data passed to subrules. Must be a 2D ndarray, with instances on rows and features on columns. Not checked at runtime for performance reasons.

required

Returns:

Type Description
ndarray

np.ndarray: The boolean result of evaluating this rule vectorized across all instances.

Examples:

>>> from hgp_lib.rules.operators import And
>>> from hgp_lib.rules import Literal
>>> import numpy as np
>>> data = np.array([
...     [True, False, True],
...     [False, False, True]
... ])
>>> rule = Or([Literal(value=0), Literal(value=1)])
>>> rule.evaluate(data)
array([ True, False])
Notes

This implementation does as few operations as possible, at the expense of more memory usage.

Source code in src/hgp_lib/rules/operators.py
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
def evaluate(self, data: np.ndarray) -> np.ndarray:
    """
    All subrules are recursively evaluated, and their results are combined using logical disjunction. The final
    result is optionally negated if `self.negated` is `True`.

    Args:
        data (np.ndarray):
            Input data passed to subrules. Must be a 2D ndarray, with instances on rows and features on columns.
            Not checked at runtime for performance reasons.

    Returns:
        np.ndarray:
            The boolean result of evaluating this rule vectorized across all instances.

    Examples:
        >>> from hgp_lib.rules.operators import And
        >>> from hgp_lib.rules import Literal
        >>> import numpy as np
        >>> data = np.array([
        ...     [True, False, True],
        ...     [False, False, True]
        ... ])
        >>> rule = Or([Literal(value=0), Literal(value=1)])
        >>> rule.evaluate(data)
        array([ True, False])

    Notes:
        This implementation does as few operations as possible, at the expense of more memory usage.
    """
    cols = []
    neg_mask = []
    sub_operators = []
    for s in self.subrules:
        if s.value is not None:  # We have a literal
            cols.append(s.value)
            neg_mask.append(s.negated)
        else:  # We have an operator
            sub_operators.append(s)

    if cols:  # Hot branch for literals
        # One-liner for all literals
        mask = (data[:, cols] ^ np.array(neg_mask)).any(1)
        # Updating with operators
        for s in sub_operators:
            mask |= s.evaluate(data)
    else:  # Hot branch for no literals
        mask = sub_operators[0].evaluate(data)  # Create an initial matrix
        for s in sub_operators[1:]:  # Updating with the rest of the operators
            mask |= s.evaluate(data)

    if self.negated:
        mask = np.logical_not(mask, out=mask)
    return mask

Rule utilities

hgp_lib.rules.utils.serialize(rule, feature_mapping=None)

Serialize a rule and optional feature names into JSON-compatible data.

Parameters:

Name Type Description Default
rule Rule

Rule tree to serialize.

required
feature_mapping dict[int, str] | None

Optional mapping from literal indices to feature names. Default: None.

None

Returns:

Type Description
dict[str, object]

dict[str, object]: Serialized feature mapping and rule tree.

Raises:

Type Description
TypeError

If the rule type or feature mapping types are unsupported.

ValueError

If a feature mapping index is negative.

Examples:

>>> from hgp_lib.rules import And, Literal
>>> from hgp_lib.rules.utils import serialize
>>> rule = And([Literal(value=0), Literal(value=1, negated=True)])
>>> serialized = serialize(rule, {0: "age", 1: "income"})
>>> serialized["feature_mapping"]
{0: 'age', 1: 'income'}
>>> serialized["rule"]
{'And': {'negated': False, 'subrules': [{'Literal': {'negated': False, 'value': 0}}, {'Literal': {'negated': True, 'value': 1}}]}}
Source code in src/hgp_lib/rules/utils.py
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
def serialize(
    rule: Rule, feature_mapping: dict[int, str] | None = None
) -> dict[str, object]:
    """
    Serialize a rule and optional feature names into JSON-compatible data.

    Args:
        rule (Rule): Rule tree to serialize.
        feature_mapping (dict[int, str] | None): Optional mapping from literal indices
            to feature names. Default: `None`.

    Returns:
        dict[str, object]: Serialized feature mapping and rule tree.

    Raises:
        TypeError: If the rule type or feature mapping types are unsupported.
        ValueError: If a feature mapping index is negative.

    Examples:
        >>> from hgp_lib.rules import And, Literal
        >>> from hgp_lib.rules.utils import serialize
        >>> rule = And([Literal(value=0), Literal(value=1, negated=True)])
        >>> serialized = serialize(rule, {0: "age", 1: "income"})
        >>> serialized["feature_mapping"]
        {0: 'age', 1: 'income'}
        >>> serialized["rule"]
        {'And': {'negated': False, 'subrules': [{'Literal': {'negated': False, 'value': 0}}, {'Literal': {'negated': True, 'value': 1}}]}}
    """
    if not isinstance(rule, Rule):
        raise TypeError(f"rule must be a Rule, is {type(rule)}")
    return {
        "feature_mapping": _serialize_feature_mapping(feature_mapping),
        "rule": _serialize_node(rule),
    }

hgp_lib.rules.utils.deserialize(serialized_rule)

Deserialize a rule and its optional feature-name mapping.

String mapping keys produced by JSON decoding are converted back to integers.

Parameters:

Name Type Description Default
serialized_rule dict[str, object]

Data produced by :func:serialize.

required

Returns:

Type Description
tuple[Rule, dict[int, str] | None]

tuple[Rule, dict[int, str] | None]: The reconstructed rule and feature mapping.

Raises:

Type Description
TypeError

If a serialized value has an invalid type.

ValueError

If the serialized structure or a mapping index is invalid.

Examples:

>>> from hgp_lib.rules import Or, Literal
>>> from hgp_lib.rules.utils import deserialize, serialize
>>> original = Or([Literal(value=0), Literal(value=2, negated=True)])
>>> restored, feature_mapping = deserialize(
...     serialize(original, {0: "age", 2: "income"})
... )
>>> str(restored)
'Or(0, ~2)'
>>> feature_mapping
{0: 'age', 2: 'income'}
>>> all(child.parent is restored for child in restored.subrules)
True
Source code in src/hgp_lib/rules/utils.py
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
def deserialize(
    serialized_rule: dict[str, object],
) -> tuple[Rule, dict[int, str] | None]:
    """
    Deserialize a rule and its optional feature-name mapping.

    String mapping keys produced by JSON decoding are converted back to integers.

    Args:
        serialized_rule (dict[str, object]): Data produced by :func:`serialize`.

    Returns:
        tuple[Rule, dict[int, str] | None]: The reconstructed rule and feature mapping.

    Raises:
        TypeError: If a serialized value has an invalid type.
        ValueError: If the serialized structure or a mapping index is invalid.

    Examples:
        >>> from hgp_lib.rules import Or, Literal
        >>> from hgp_lib.rules.utils import deserialize, serialize
        >>> original = Or([Literal(value=0), Literal(value=2, negated=True)])
        >>> restored, feature_mapping = deserialize(
        ...     serialize(original, {0: "age", 2: "income"})
        ... )
        >>> str(restored)
        'Or(0, ~2)'
        >>> feature_mapping
        {0: 'age', 2: 'income'}
        >>> all(child.parent is restored for child in restored.subrules)
        True
    """
    if not isinstance(serialized_rule, dict):
        raise TypeError("serialized_rule must be a dictionary")
    if set(serialized_rule) != {"feature_mapping", "rule"}:
        raise ValueError("serialized_rule must contain feature_mapping and rule")

    feature_mapping = _deserialize_feature_mapping(serialized_rule["feature_mapping"])
    rule = _deserialize_node(serialized_rule["rule"])
    return rule, feature_mapping