Skip to content

quast_decisiontree.core.node

quast_decisiontree.core.node

logger module-attribute

logger = logging.getLogger('dt_logger')

Node

Bases: ABC

Base class for all nodes in the decision tree.

A node is the unit of execution in the tree: it runs some logic, optionally interacts with the user, and decides which node to visit next. Concrete nodes subclass this and implement :meth:execute and :meth:next_node.

Source code in src/quast_decisiontree/core/node.py
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
class Node(ABC):
    """Base class for all nodes in the decision tree.

    A node is the unit of execution in the tree: it runs some logic, optionally
    interacts with the user, and decides which node to visit next. Concrete nodes
    subclass this and implement :meth:`execute` and :meth:`next_node`.
    """

    # Names of children this node can select between. Used by check_children to
    # verify that path selection will work for multi-child nodes.
    _known_children = tuple()
    # Accepted values for default_policy. Empty means "accept anything".
    _known_default_policies = tuple()
    # Path keys this node understands, in the format
    # {key: (dtype, [option1, option2, ...])}:
    #   key: the key within the path dictionary
    #   dtype: required type of the value associated to that key
    #   list of options: possible values if there is a finite amount of them
    _path_keys = {}

    def __init__(
        self,
        requires: list[str],
        creates: list[str],
        children: list[str],
        *,
        default_policy: str | None = None,
        name: str | None = None,
        description: str | None = None,
        validate_children: bool = True,
        provides_backend: bool = False,
        silent: bool = False,
        final: bool = False,
    ):
        """Initializes a node of the decision tree. The node is the base unit of all code
        execution within the tree. During initialization, it is incorporated into the structure
        of the decision tree instance. Typically, a concrete implementations of this abstract
        base class should respect the following:

        - the `requires` and `creates` arguments should be fixed in their __init__() method (that
            is, they should *not* be allowed to change when creating instances of the class). For
            the form and nature of the arguments, see below.
        - the subclass __init__() method should always call super().__init__() as the first thing
            it does, while passing the necessary arguments. This ensures the members the
            decisiontree expects are properly initialized.

        `requires` and `creates` provide an interface for the decisiontree in order to make sure
        the nodes get the right data they produce. They contain the keys of entries in the
        specialized DecisionTreeProblemData dictionary defined in
        decisiontree/utils/problem_data.py. This dictionary will not only check the existence
        of these keys when executing the decisiontree, but is also able to perform more advanced
        checks. These are coded by the key string itself. For example, a value indexed by the key
        "instance_file" needs to pass through two checks: (1) whether it is a string and
        (2) whether it represents a path to an existing file. For the definition of checks for
        basic keys, see decisiontree/utils/problem_data_basic_keys.py. To add new keys, use the
        class method DecisionTreeProblemData.add_known_keys(args) defined in decisiontree/utils/
        problem_data.py.

        Specifics for the individual arguments:
        - creates: either a single key, or a list of keys
        - requires: The requirements are given in conjunctive normal form (an "and of ors")
            represented by a nested list. Its base template is [[key1, key2], [key3]] which
            translates to the requirement "(key1 or key2) and key3)". For convenience, some
            shortcuts are implemented, with the following logical behavior: (1) passing a
            simple (non-nested) list [key1, key2, key3] will translate to "key1 and key2
            and key3" and (2) passing a single key key1 will translate to [[key1]].

            Additionally, there is a workaround to define optional dependencies. These will
            not enter the validation process of the decision tree (where the tree determines
            whether the required problem data entries are present at all junctions). If an
            or clause begins with "optional:", it will be treated as a list of optional keys.
            A requirement list can only contain one such clause. E.g., a requirement such as
            [["problem_instance", "instance_file"], ["optional:", "problem_class"]] will
            translate to: "This node needs either "problem_instance" or "instance_file", and
            will make use of "problem_class" if it's present (but doesn't require it).
        """
        self._final = final
        self._root = False
        self.requires = requires  # attention: this will also set the self.optional member
        self.creates = creates
        self.name = name  # None is replaced with the class name by the setter
        self.description = description  # None is replaced with the class docstring by the setter
        self._provides_backend = provides_backend
        self.silent = silent
        self.request_info = None

        self.children = children
        if validate_children:
            if not self.check_children():
                logger.warning(
                    "There is more than one child to %s, and not all children are known.",
                    self.__class__.__name__,
                )
                logger.warning("Path selection might not work.")
        self.default_policy = default_policy

    def path_spec(self) -> dict[str, dict]:
        """Returns a {key: {dtype[, options]}} mapping for this node's path keys.

        Omits ``options`` entries that are ``None`` for a cleaner spec output.
        """
        return {
            key: {k: v for k, v in path_key.to_dict().items() if v is not None}
            for key, path_key in self._path_keys.items()
        }

    @abstractmethod
    def execute(self, problem_data: dict, path_info: dict) -> dict:
        """executes all that is to do and decide at the current node

        The parent node needs to ensure the problem_data is compatible.

        Output: a dictionary containing the path info necessary to define the next node.
        """

    def next_node(self, next_node_info: dict) -> str:
        """Returns the successor node for the given ``next_node_info``. Default
        behavior is to return the first child. Override in subclasses if needed.

        See also: FinalNode template for nodes without children.

        Concrete nodes typically return the child's name as a string, which the
        decision tree resolves to the actual node instance. For every possible
        ``next_node_info`` returned by :meth:`execute`, this must return a
        successor unless the node is final.
        """
        return self.children[0]

    @property
    def request_info(self):
        if self._request_info is None:
            raise NoRequestFunctionError("Request function of this node has not been set.")
        return self._request_info

    @request_info.setter
    def request_info(self, request_func: Callable):
        self._request_info = request_func

    @property
    def provides_backend(self):
        return self._provides_backend

    @property
    def description(self):
        return self._description

    @description.setter
    def description(self, value):
        if value is None:
            self._description = self.__class__.__doc__
            if self._description is None:
                self._description = ""
        else:
            self._description = value

    @property
    def creates(self):
        """property containing the problem data entries this node creates"""
        return self._creates

    @creates.setter
    def creates(self, val):
        """sets the problem data entries this node creates"""
        if isinstance(val, str):
            self._creates = [val]
        elif isinstance(val, list):
            self._creates = val
        else:
            raise ValueError(
                f"{val!r} is neither string nor list and can not be written to the "
                "_creates attribute of the node."
            )

    @property
    def requires(self):
        """property containing the problem data entries this node requires"""
        return self._requires

    @requires.setter
    def requires(self, requires):
        """setting the requirements, transforming into conjunctive normal form"""
        self._requires = cnf_list(requires)
        self.optional = None
        for or_clause in self._requires:
            if or_clause[0] == "optional:":
                if self.optional is None:
                    self.optional = or_clause[1:]
                else:
                    raise ValueError(
                        f"requirement clause {requires!r} contains more than one list of "
                        "optional keys."
                    )
        if self.optional is None:
            self.optional = []

    @property
    def children(self):
        """property containing all possible children of this node"""
        return self._children

    @children.setter
    def children(self, children):
        """stores the children's names in the class.

        If nodes are passed, their names are retrieved first
        """
        children_names = []
        for child in children:
            if isinstance(child, Node):
                children_names.append(child._name)
            elif isinstance(child, str):
                children_names.append(child)
            else:
                raise ValueError(
                    f"Trying to set children, but list element {child!r} is neither Node nor "
                    "string."
                )
        self._children = children_names

    def check_children(self) -> bool:
        """checks whether there is either one child, or all children are known to the node (via
        the _known_children class property) and therefore the path selection is working
        """
        if len(self._known_children) == 0:
            logger.debug("No entries in %s._known_children. Skipping check.", type(self).__name__)
            return True
        if len(self.children) == 1:
            return True
        elif len(self.children) == 0 and self.final:
            return True
        else:
            return all(child in self._known_children for child in self.children)

    @property
    def name(self):
        """containing the name of a node for bookkeeping purposes"""
        return self._name

    @name.setter
    def name(self, name: str | None = None):
        """sets the name of the node instance.

        A name can either be given explicitly, or derived from the class name.
        """
        if name is None:
            self._name = self.__class__.__name__
        else:
            self._name = name

    @property
    def final(self):
        """whether the node is final"""
        return self._final

    @final.setter
    def final(self, final: bool):
        """sets the value of the final flag"""
        self._final = final

    @property
    def root(self):
        """whether the node is at the root of the decision tree"""
        return self._root

    @root.setter
    def root(self, root: bool):
        self._root = root

    @property
    def default_policy(self):
        return self._default_policy

    @default_policy.setter
    def default_policy(self, value):
        if (
            value is None
            or value in self._known_default_policies
            or len(self._known_default_policies) == 0
        ):
            self._default_policy = value
        else:
            raise ValueError(
                f"Unknown default policy {value!r} given.\n"
                f"Known default policies: \n {self._known_default_policies}"
            )

    def interpret_result(
        self,
        result: dict,
        problem_data: dict,
        next_node_info: dict | None = None,
        config: dict | None = None,
    ) -> dict:
        """interprets the result in a form given by the next node and returns it in a
        form understood by all parent nodes.

        By default, results are just passed through.
        """
        return result

    def is_input_valid(self, in_keys: list) -> bool:
        return not self.violated_requirements(in_keys)

    def violated_requirements(self, in_keys: list) -> list:
        out = []
        for or_clause in self.requires:
            if or_clause[0] == "optional:":  # signals optional problem data entries
                continue
            if not (set(or_clause) & set(in_keys)):
                out.append(or_clause)
        return out

silent instance-attribute

silent = silent

request_info property writable

request_info

provides_backend property

provides_backend

description property writable

description

creates property writable

creates

property containing the problem data entries this node creates

requires property writable

requires

property containing the problem data entries this node requires

children property writable

children

property containing all possible children of this node

name property writable

name

containing the name of a node for bookkeeping purposes

final property writable

final

whether the node is final

root property writable

root

whether the node is at the root of the decision tree

default_policy property writable

default_policy

__init__

__init__(
    requires,
    creates,
    children,
    *,
    default_policy=None,
    name=None,
    description=None,
    validate_children=True,
    provides_backend=False,
    silent=False,
    final=False,
)

Initializes a node of the decision tree. The node is the base unit of all code execution within the tree. During initialization, it is incorporated into the structure of the decision tree instance. Typically, a concrete implementations of this abstract base class should respect the following:

  • the requires and creates arguments should be fixed in their init() method (that is, they should not be allowed to change when creating instances of the class). For the form and nature of the arguments, see below.
  • the subclass init() method should always call super().init() as the first thing it does, while passing the necessary arguments. This ensures the members the decisiontree expects are properly initialized.

requires and creates provide an interface for the decisiontree in order to make sure the nodes get the right data they produce. They contain the keys of entries in the specialized DecisionTreeProblemData dictionary defined in decisiontree/utils/problem_data.py. This dictionary will not only check the existence of these keys when executing the decisiontree, but is also able to perform more advanced checks. These are coded by the key string itself. For example, a value indexed by the key "instance_file" needs to pass through two checks: (1) whether it is a string and (2) whether it represents a path to an existing file. For the definition of checks for basic keys, see decisiontree/utils/problem_data_basic_keys.py. To add new keys, use the class method DecisionTreeProblemData.add_known_keys(args) defined in decisiontree/utils/ problem_data.py.

Specifics for the individual arguments: - creates: either a single key, or a list of keys - requires: The requirements are given in conjunctive normal form (an "and of ors") represented by a nested list. Its base template is [[key1, key2], [key3]] which translates to the requirement "(key1 or key2) and key3)". For convenience, some shortcuts are implemented, with the following logical behavior: (1) passing a simple (non-nested) list [key1, key2, key3] will translate to "key1 and key2 and key3" and (2) passing a single key key1 will translate to [[key1]].

Additionally, there is a workaround to define optional dependencies. These will
not enter the validation process of the decision tree (where the tree determines
whether the required problem data entries are present at all junctions). If an
or clause begins with "optional:", it will be treated as a list of optional keys.
A requirement list can only contain one such clause. E.g., a requirement such as
[["problem_instance", "instance_file"], ["optional:", "problem_class"]] will
translate to: "This node needs either "problem_instance" or "instance_file", and
will make use of "problem_class" if it's present (but doesn't require it).
Source code in src/quast_decisiontree/core/node.py
 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
def __init__(
    self,
    requires: list[str],
    creates: list[str],
    children: list[str],
    *,
    default_policy: str | None = None,
    name: str | None = None,
    description: str | None = None,
    validate_children: bool = True,
    provides_backend: bool = False,
    silent: bool = False,
    final: bool = False,
):
    """Initializes a node of the decision tree. The node is the base unit of all code
    execution within the tree. During initialization, it is incorporated into the structure
    of the decision tree instance. Typically, a concrete implementations of this abstract
    base class should respect the following:

    - the `requires` and `creates` arguments should be fixed in their __init__() method (that
        is, they should *not* be allowed to change when creating instances of the class). For
        the form and nature of the arguments, see below.
    - the subclass __init__() method should always call super().__init__() as the first thing
        it does, while passing the necessary arguments. This ensures the members the
        decisiontree expects are properly initialized.

    `requires` and `creates` provide an interface for the decisiontree in order to make sure
    the nodes get the right data they produce. They contain the keys of entries in the
    specialized DecisionTreeProblemData dictionary defined in
    decisiontree/utils/problem_data.py. This dictionary will not only check the existence
    of these keys when executing the decisiontree, but is also able to perform more advanced
    checks. These are coded by the key string itself. For example, a value indexed by the key
    "instance_file" needs to pass through two checks: (1) whether it is a string and
    (2) whether it represents a path to an existing file. For the definition of checks for
    basic keys, see decisiontree/utils/problem_data_basic_keys.py. To add new keys, use the
    class method DecisionTreeProblemData.add_known_keys(args) defined in decisiontree/utils/
    problem_data.py.

    Specifics for the individual arguments:
    - creates: either a single key, or a list of keys
    - requires: The requirements are given in conjunctive normal form (an "and of ors")
        represented by a nested list. Its base template is [[key1, key2], [key3]] which
        translates to the requirement "(key1 or key2) and key3)". For convenience, some
        shortcuts are implemented, with the following logical behavior: (1) passing a
        simple (non-nested) list [key1, key2, key3] will translate to "key1 and key2
        and key3" and (2) passing a single key key1 will translate to [[key1]].

        Additionally, there is a workaround to define optional dependencies. These will
        not enter the validation process of the decision tree (where the tree determines
        whether the required problem data entries are present at all junctions). If an
        or clause begins with "optional:", it will be treated as a list of optional keys.
        A requirement list can only contain one such clause. E.g., a requirement such as
        [["problem_instance", "instance_file"], ["optional:", "problem_class"]] will
        translate to: "This node needs either "problem_instance" or "instance_file", and
        will make use of "problem_class" if it's present (but doesn't require it).
    """
    self._final = final
    self._root = False
    self.requires = requires  # attention: this will also set the self.optional member
    self.creates = creates
    self.name = name  # None is replaced with the class name by the setter
    self.description = description  # None is replaced with the class docstring by the setter
    self._provides_backend = provides_backend
    self.silent = silent
    self.request_info = None

    self.children = children
    if validate_children:
        if not self.check_children():
            logger.warning(
                "There is more than one child to %s, and not all children are known.",
                self.__class__.__name__,
            )
            logger.warning("Path selection might not work.")
    self.default_policy = default_policy

path_spec

path_spec()

Returns a {key: {dtype[, options]}} mapping for this node's path keys.

Omits options entries that are None for a cleaner spec output.

Source code in src/quast_decisiontree/core/node.py
121
122
123
124
125
126
127
128
129
def path_spec(self) -> dict[str, dict]:
    """Returns a {key: {dtype[, options]}} mapping for this node's path keys.

    Omits ``options`` entries that are ``None`` for a cleaner spec output.
    """
    return {
        key: {k: v for k, v in path_key.to_dict().items() if v is not None}
        for key, path_key in self._path_keys.items()
    }

execute abstractmethod

execute(problem_data, path_info)

executes all that is to do and decide at the current node

The parent node needs to ensure the problem_data is compatible.

Output: a dictionary containing the path info necessary to define the next node.

Source code in src/quast_decisiontree/core/node.py
131
132
133
134
135
136
137
138
@abstractmethod
def execute(self, problem_data: dict, path_info: dict) -> dict:
    """executes all that is to do and decide at the current node

    The parent node needs to ensure the problem_data is compatible.

    Output: a dictionary containing the path info necessary to define the next node.
    """

next_node

next_node(next_node_info)

Returns the successor node for the given next_node_info. Default behavior is to return the first child. Override in subclasses if needed.

See also: FinalNode template for nodes without children.

Concrete nodes typically return the child's name as a string, which the decision tree resolves to the actual node instance. For every possible next_node_info returned by :meth:execute, this must return a successor unless the node is final.

Source code in src/quast_decisiontree/core/node.py
140
141
142
143
144
145
146
147
148
149
150
151
def next_node(self, next_node_info: dict) -> str:
    """Returns the successor node for the given ``next_node_info``. Default
    behavior is to return the first child. Override in subclasses if needed.

    See also: FinalNode template for nodes without children.

    Concrete nodes typically return the child's name as a string, which the
    decision tree resolves to the actual node instance. For every possible
    ``next_node_info`` returned by :meth:`execute`, this must return a
    successor unless the node is final.
    """
    return self.children[0]

check_children

check_children()

checks whether there is either one child, or all children are known to the node (via the _known_children class property) and therefore the path selection is working

Source code in src/quast_decisiontree/core/node.py
244
245
246
247
248
249
250
251
252
253
254
255
256
def check_children(self) -> bool:
    """checks whether there is either one child, or all children are known to the node (via
    the _known_children class property) and therefore the path selection is working
    """
    if len(self._known_children) == 0:
        logger.debug("No entries in %s._known_children. Skipping check.", type(self).__name__)
        return True
    if len(self.children) == 1:
        return True
    elif len(self.children) == 0 and self.final:
        return True
    else:
        return all(child in self._known_children for child in self.children)

interpret_result

interpret_result(
    result, problem_data, next_node_info=None, config=None
)

interprets the result in a form given by the next node and returns it in a form understood by all parent nodes.

By default, results are just passed through.

Source code in src/quast_decisiontree/core/node.py
311
312
313
314
315
316
317
318
319
320
321
322
323
def interpret_result(
    self,
    result: dict,
    problem_data: dict,
    next_node_info: dict | None = None,
    config: dict | None = None,
) -> dict:
    """interprets the result in a form given by the next node and returns it in a
    form understood by all parent nodes.

    By default, results are just passed through.
    """
    return result

is_input_valid

is_input_valid(in_keys)
Source code in src/quast_decisiontree/core/node.py
325
326
def is_input_valid(self, in_keys: list) -> bool:
    return not self.violated_requirements(in_keys)

violated_requirements

violated_requirements(in_keys)
Source code in src/quast_decisiontree/core/node.py
328
329
330
331
332
333
334
335
def violated_requirements(self, in_keys: list) -> list:
    out = []
    for or_clause in self.requires:
        if or_clause[0] == "optional:":  # signals optional problem data entries
            continue
        if not (set(or_clause) & set(in_keys)):
            out.append(or_clause)
    return out

FinalNode

Bases: Node, ABC

Base class for terminal nodes of the tree.

A final node produces a result in :meth:execute and has no successor; calling :meth:next_node raises :class:FinalNodeError.

Source code in src/quast_decisiontree/core/node.py
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
class FinalNode(Node, ABC):
    """Base class for terminal nodes of the tree.

    A final node produces a result in :meth:`execute` and has no successor;
    calling :meth:`next_node` raises :class:`FinalNodeError`.
    """

    def __init__(self, *args, **kwargs):
        """Initializes a final node with no children, forcing final=True."""
        if "children" in kwargs.keys():
            del kwargs["children"]
        super().__init__(*args, children=[], **kwargs)
        self.final = True
        self.root = False

    @abstractmethod
    def execute(self, problem_data: dict, path_info: dict) -> dict:
        """executes the final algorithm read from the problem_data in the current
        form and return a path_info dictionary
        """

    def next_node(self, next_node_info: dict) -> NoReturn:
        """raises an error since this node is final"""
        raise FinalNodeError("Calling next_node() method of a final node. Check node.final first.")

    def interpret_result(
        self,
        result: dict,
        problem_data: dict,
        next_node_info: dict | None = None,
        config: dict | None = None,
    ) -> dict:
        """since the execute method of final nodes already returns a result, this is a trivial
        safeguard
        """
        return result

final instance-attribute

final = True

root instance-attribute

root = False

__init__

__init__(*args, **kwargs)

Initializes a final node with no children, forcing final=True.

Source code in src/quast_decisiontree/core/node.py
345
346
347
348
349
350
351
def __init__(self, *args, **kwargs):
    """Initializes a final node with no children, forcing final=True."""
    if "children" in kwargs.keys():
        del kwargs["children"]
    super().__init__(*args, children=[], **kwargs)
    self.final = True
    self.root = False

execute abstractmethod

execute(problem_data, path_info)

executes the final algorithm read from the problem_data in the current form and return a path_info dictionary

Source code in src/quast_decisiontree/core/node.py
353
354
355
356
357
@abstractmethod
def execute(self, problem_data: dict, path_info: dict) -> dict:
    """executes the final algorithm read from the problem_data in the current
    form and return a path_info dictionary
    """

next_node

next_node(next_node_info)

raises an error since this node is final

Source code in src/quast_decisiontree/core/node.py
359
360
361
def next_node(self, next_node_info: dict) -> NoReturn:
    """raises an error since this node is final"""
    raise FinalNodeError("Calling next_node() method of a final node. Check node.final first.")

interpret_result

interpret_result(
    result, problem_data, next_node_info=None, config=None
)

since the execute method of final nodes already returns a result, this is a trivial safeguard

Source code in src/quast_decisiontree/core/node.py
363
364
365
366
367
368
369
370
371
372
373
def interpret_result(
    self,
    result: dict,
    problem_data: dict,
    next_node_info: dict | None = None,
    config: dict | None = None,
) -> dict:
    """since the execute method of final nodes already returns a result, this is a trivial
    safeguard
    """
    return result

MessageNode

Bases: Node

A node that only logs a message and forwards to its single child.

Source code in src/quast_decisiontree/core/node.py
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
class MessageNode(Node):
    """A node that only logs a message and forwards to its single child."""

    def __init__(
        self,
        message: str,
        children: list[str],
        backward_message: str | None = None,
        *args,
        **kwargs,
    ):
        """Creates a node that logs a message and does nothing else.

        Args:
            message: the message to log on the forward path.
            children: a list containing exactly one child node (or its name).
            backward_message: the message to log on the backward path. Defaults to None.

        Raises:
            ValueError: if more than one child is given.
        """
        if len(children) > 1:
            raise ValueError("MessageNode cannot have more than one child")
        super().__init__(*args, children=children, requires=[], creates=[], **kwargs)
        self.message = message
        self.backward_message = backward_message

    def execute(self, problem_data: dict, path_info: dict) -> dict:
        logger.info(self.message)
        return {}

    def next_node(self, next_node_info: dict) -> str:
        return self.children[0]

    def interpret_result(
        self,
        result: dict,
        problem_data: dict,
        next_node_info: dict | None = None,
        config: dict | None = None,
    ) -> dict:
        if self.backward_message is not None:
            logger.info(self.backward_message)
        return result

message instance-attribute

message = message

backward_message instance-attribute

backward_message = backward_message

__init__

__init__(
    message,
    children,
    backward_message=None,
    *args,
    **kwargs,
)

Creates a node that logs a message and does nothing else.

Parameters:

Name Type Description Default
message str

the message to log on the forward path.

required
children list[str]

a list containing exactly one child node (or its name).

required
backward_message str | None

the message to log on the backward path. Defaults to None.

None

Raises:

Type Description
ValueError

if more than one child is given.

Source code in src/quast_decisiontree/core/node.py
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
def __init__(
    self,
    message: str,
    children: list[str],
    backward_message: str | None = None,
    *args,
    **kwargs,
):
    """Creates a node that logs a message and does nothing else.

    Args:
        message: the message to log on the forward path.
        children: a list containing exactly one child node (or its name).
        backward_message: the message to log on the backward path. Defaults to None.

    Raises:
        ValueError: if more than one child is given.
    """
    if len(children) > 1:
        raise ValueError("MessageNode cannot have more than one child")
    super().__init__(*args, children=children, requires=[], creates=[], **kwargs)
    self.message = message
    self.backward_message = backward_message

execute

execute(problem_data, path_info)
Source code in src/quast_decisiontree/core/node.py
403
404
405
def execute(self, problem_data: dict, path_info: dict) -> dict:
    logger.info(self.message)
    return {}

next_node

next_node(next_node_info)
Source code in src/quast_decisiontree/core/node.py
407
408
def next_node(self, next_node_info: dict) -> str:
    return self.children[0]

interpret_result

interpret_result(
    result, problem_data, next_node_info=None, config=None
)
Source code in src/quast_decisiontree/core/node.py
410
411
412
413
414
415
416
417
418
419
def interpret_result(
    self,
    result: dict,
    problem_data: dict,
    next_node_info: dict | None = None,
    config: dict | None = None,
) -> dict:
    if self.backward_message is not None:
        logger.info(self.backward_message)
    return result

BranchingNode

Bases: Node

a node that does nothing but ask for a decision to which node to go next

Source code in src/quast_decisiontree/core/node.py
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
class BranchingNode(Node):
    """a node that does nothing but ask for a decision to which node to go next"""

    def __init__(
        self,
        question: str,
        keys: list,
        descriptions: list,
        children: list[str],
        *args,
        **kwargs,
    ):
        """Creates a branching node that lets the user select one of several successors.

        ``keys``, ``descriptions`` and ``children`` must all have the same length,
        with a one-to-one correspondence between their elements.

        Args:
            question: the string shown to the user when asking for input.
            keys: the possible options the user can choose from.
            descriptions: a description matching each key.
            children: the candidate successors, one of which the user selects.

        Raises:
            ValueError: if the lists provided don't all have the same length.
        """
        if len(keys) != len(children) or len(keys) != len(descriptions):
            raise ValueError(
                "Number of possible next nodes must be the same as the number of possible "
                "answers and nodes."
            )
        else:
            super().__init__(
                *args,
                children=children,
                creates=[],
                requires=[],
                **kwargs,
            )
            self.next_nodes = dict(zip(keys, children, strict=False))
            self.query = MultiChoiceQuery(
                question=question, answers=dict(zip(keys, descriptions, strict=False))
            )

    def execute(self, problem_data: dict, path_info: dict) -> dict:
        chosen = self.query.input()
        return dict(chosen_option=chosen)

    def next_node(self, next_node_info: dict) -> str:
        return self.next_nodes[next_node_info["chosen_option"]]

next_nodes instance-attribute

next_nodes = dict(zip(keys, children, strict=False))

query instance-attribute

query = MultiChoiceQuery(
    question=question,
    answers=dict(zip(keys, descriptions, strict=False)),
)

__init__

__init__(
    question, keys, descriptions, children, *args, **kwargs
)

Creates a branching node that lets the user select one of several successors.

keys, descriptions and children must all have the same length, with a one-to-one correspondence between their elements.

Parameters:

Name Type Description Default
question str

the string shown to the user when asking for input.

required
keys list

the possible options the user can choose from.

required
descriptions list

a description matching each key.

required
children list[str]

the candidate successors, one of which the user selects.

required

Raises:

Type Description
ValueError

if the lists provided don't all have the same length.

Source code in src/quast_decisiontree/core/node.py
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
def __init__(
    self,
    question: str,
    keys: list,
    descriptions: list,
    children: list[str],
    *args,
    **kwargs,
):
    """Creates a branching node that lets the user select one of several successors.

    ``keys``, ``descriptions`` and ``children`` must all have the same length,
    with a one-to-one correspondence between their elements.

    Args:
        question: the string shown to the user when asking for input.
        keys: the possible options the user can choose from.
        descriptions: a description matching each key.
        children: the candidate successors, one of which the user selects.

    Raises:
        ValueError: if the lists provided don't all have the same length.
    """
    if len(keys) != len(children) or len(keys) != len(descriptions):
        raise ValueError(
            "Number of possible next nodes must be the same as the number of possible "
            "answers and nodes."
        )
    else:
        super().__init__(
            *args,
            children=children,
            creates=[],
            requires=[],
            **kwargs,
        )
        self.next_nodes = dict(zip(keys, children, strict=False))
        self.query = MultiChoiceQuery(
            question=question, answers=dict(zip(keys, descriptions, strict=False))
        )

execute

execute(problem_data, path_info)
Source code in src/quast_decisiontree/core/node.py
466
467
468
def execute(self, problem_data: dict, path_info: dict) -> dict:
    chosen = self.query.input()
    return dict(chosen_option=chosen)

next_node

next_node(next_node_info)
Source code in src/quast_decisiontree/core/node.py
470
471
def next_node(self, next_node_info: dict) -> str:
    return self.next_nodes[next_node_info["chosen_option"]]

BackendNode

Bases: Node

A node that provides one or more quantum or quantum-classical backends.

Backend nodes are silent by default and advertise provides_backend=True so the tree can route backend requests from other nodes to them.

Source code in src/quast_decisiontree/core/node.py
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
class BackendNode(Node):
    """A node that provides one or more quantum or quantum-classical backends.

    Backend nodes are silent by default and advertise ``provides_backend=True`` so
    the tree can route backend requests from other nodes to them.
    """

    def __init__(
        self,
        requires: list[str],
        creates: list[str],
        children: list[str],
        *args,
        silent: bool = True,
        **kwargs,
    ) -> None:
        super().__init__(
            *args,
            requires=requires,
            creates=creates,
            children=children,
            silent=silent,
            provides_backend=True,
            **kwargs,
        )
        self._backends = BackendProvider()

    @property
    def backends(self):
        return self._backends

    @abstractmethod
    def get_backends(self) -> BackendProvider:
        """this method should return a BackendProvider with the possible backends"""

    @abstractmethod
    def get_submit_func(self, backend_name: str) -> Callable:
        """this method should return a callable that allows other nodes to submit single quantum
        circuits
        """

backends property

backends

__init__

__init__(
    requires,
    creates,
    children,
    *args,
    silent=True,
    **kwargs,
)
Source code in src/quast_decisiontree/core/node.py
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
def __init__(
    self,
    requires: list[str],
    creates: list[str],
    children: list[str],
    *args,
    silent: bool = True,
    **kwargs,
) -> None:
    super().__init__(
        *args,
        requires=requires,
        creates=creates,
        children=children,
        silent=silent,
        provides_backend=True,
        **kwargs,
    )
    self._backends = BackendProvider()

get_backends abstractmethod

get_backends()

this method should return a BackendProvider with the possible backends

Source code in src/quast_decisiontree/core/node.py
508
509
510
@abstractmethod
def get_backends(self) -> BackendProvider:
    """this method should return a BackendProvider with the possible backends"""

get_submit_func abstractmethod

get_submit_func(backend_name)

this method should return a callable that allows other nodes to submit single quantum circuits

Source code in src/quast_decisiontree/core/node.py
512
513
514
515
516
@abstractmethod
def get_submit_func(self, backend_name: str) -> Callable:
    """this method should return a callable that allows other nodes to submit single quantum
    circuits
    """