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 Optional[List[Rule]]

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

parent Optional[Rule]

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

value Optional[int]

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 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
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 (Optional[List[Rule]]):
            The list of child rules, for operators, or `None`, for literals. Default: `None`.
        parent (Optional[Rule]):
            A reference to the parent rule in the tree (if any). Default: `None`.
        value (Optional[int]):
            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__ = ("subrules", "parent", "value", "negated")

    def __init__(
        self,
        subrules: Optional[List["Rule"]] = None,
        parent: Optional["Rule"] = None,
        value: Optional[int] = 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: Dict[int, 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 (Dict[int, str] | None): The feature names that can be used to replace literal values when
                provided. 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({1: "good", 2:"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: Optional["Rule"] = None) -> "Rule":
        """
        Creates a deep copy of this rule and its entire subtree, optionally assigning a new parent.

        Args:
            parent (Optional[Rule]):
                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
        """

        return self.__class__(
            self.subrules,
            self.parent if parent is None else parent,
            self.value,
            self.negated,
        )

    @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.
        """
        pass

    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 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 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 Dict[int, str] | None

The feature names that can be used to replace literal values when provided. 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({1: "good", 2:"nice"})
'~And(good, nice)'
Source code in 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
def to_str(
    self, feature_names: Dict[int, 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 (Dict[int, str] | None): The feature names that can be used to replace literal values when
            provided. 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({1: "good", 2:"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 Optional[Rule]

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 hgp_lib\rules\rules.py
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
def copy(self, parent: Optional["Rule"] = None) -> "Rule":
    """
    Creates a deep copy of this rule and its entire subtree, optionally assigning a new parent.

    Args:
        parent (Optional[Rule]):
            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
    """

    return self.__class__(
        self.subrules,
        self.parent if parent is None else parent,
        self.value,
        self.negated,
    )

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 hgp_lib\rules\rules.py
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
@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.
    """
    pass

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 hgp_lib\rules\rules.py
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
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 Optional[List[Rule]]

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

parent Optional[Rule]

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 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
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 (Optional[List[Rule]]):
            The list of child rules, for operators, or `None`, for literals. Default: `None`.
        parent (Optional[Rule]):
            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: Dict[int, str] | None = None, indent: bool = -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 (Dict[int, str] | None): The feature names that can be used to replace literal values when
                provided. 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({0: "bad"})
            '~bad'
        """
        value = self.value
        if feature_names is not None:
            value = feature_names[value]
        return f"~{value}" if self.negated else f"{value}"

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 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 Dict[int, str] | None

The feature names that can be used to replace literal values when provided. 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({0: "bad"})
'~bad'
Source code in 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
def to_str(
    self, feature_names: Dict[int, str] | None = None, indent: bool = -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 (Dict[int, str] | None): The feature names that can be used to replace literal values when
            provided. 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({0: "bad"})
        '~bad'
    """
    value = self.value
    if feature_names is not None:
        value = feature_names[value]
    return f"~{value}" if self.negated else f"{value}"

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 Optional[Rule]

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 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 (Optional[Rule]):
            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 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 Optional[Rule]

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 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 (Optional[Rule]):
            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 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