a node describes an operation call event a keras function is a dag with node instances as nodes and kerastensor instances as edges nodes aren t operation instances because a single operation could be called multiple times which would result in graph cycles a call event involves input tensors and other input arguments the operation that was called and the resulting output tensors a node will include all this information since a single operation could be called multiple times the node instances are stored on operations as a list each time an operation is called a node is added to op inboundnodes each time the output of an operation is used by another operation a node is added to op outboundnodes every kerastensor instance has a kerashistory object attached which tracks the node that records the call event that created the tensor by recursively walking through node instances via the kerashistory metadata of kerastensor instances once can retrieve the entire dag of a keras function args operation the operation that was called in the op call event that this node represents callargs the positional arguments the operation was called with callkwargs the keyword arguments the operation was called with outputs the output tensors of the op call call if inputs don t have metadata yet add it wire up node to operations set metadata on outputs whether this is a root node the parent nodes returns all the nodes whose output this node immediately depends on tracks the operation call that created a tensor during construction of keras functions this metadata is added to each tensor produced as the output of an operation this allows keras to track how each tensor was produced and this information is later retraced by the function class to reconstruct the operations graph attributes operation the operation instance that produced the tensor nodeindex the specific call to the operation that produced this tensor operations can be called multiple times in order to share weights a new node is created every time an operation is called the corresponding node that represents the call event that produced the tensor can be found at op inboundnodesnodeindex tensorindex the output index for this tensor always zero if the operation that produced this tensor only has one output nested structures of tensors are deterministically assigned an index via nest flatten added to maintain memory and performance characteristics of namedtuple while subclassing a node describes an operation __call__ event a keras function is a dag with node instances as nodes and kerastensor instances as edges nodes aren t operation instances because a single operation could be called multiple times which would result in graph cycles a __call__ event involves input tensors and other input arguments the operation that was called and the resulting output tensors a node will include all this information since a single operation could be called multiple times the node instances are stored on operations as a list each time an operation is called a node is added to op _inbound_nodes each time the output of an operation is used by another operation a node is added to op _outbound_nodes every kerastensor instance has a kerashistory object attached which tracks the node that records the __call__ event that created the tensor by recursively walking through node instances via the kerashistory metadata of kerastensor instances once can retrieve the entire dag of a keras function args operation the operation that was called in the op __call__ event that this node represents call_args the positional arguments the operation was called with call_kwargs the keyword arguments the operation was called with outputs the output tensors of the op __call__ call if inputs don t have metadata yet add it wire up node to operations it s a graph entry point set metadata on outputs whether this is a root node the parent node s returns all the node s whose output this node immediately depends on none for input tensors tracks the operation call that created a tensor during construction of keras functions this metadata is added to each tensor produced as the output of an operation this allows keras to track how each tensor was produced and this information is later retraced by the function class to reconstruct the operations graph attributes operation the operation instance that produced the tensor node_index the specific call to the operation that produced this tensor operations can be called multiple times in order to share weights a new node is created every time an operation is called the corresponding node that represents the call event that produced the tensor can be found at op _inbound_nodes node_index tensor_index the output index for this tensor always zero if the operation that produced this tensor only has one output nested structures of tensors are deterministically assigned an index via nest flatten added to maintain memory and performance characteristics of namedtuple while subclassing
import collections

import tree

from keras.backend import KerasTensor
from keras.ops.symbolic_arguments import SymbolicArguments


class Node:
    

    def __init__(
        self, operation, call_args=None, call_kwargs=None, outputs=None
    ):
        self.operation = operation
        self.arguments = SymbolicArguments(*call_args, **call_kwargs)
        self.outputs = [] if outputs is None else tree.flatten(outputs)
        for x in self.outputs:
            if not isinstance(x, KerasTensor):
                raise ValueError(
                    "All operation outputs must be tensors. "
                    f"Operation {operation} returned a non-tensor. "
                    f"Non-tensor received: {x}"
                )

        zero_history = any(
            not x.record_history for x in self.arguments.keras_tensors
        )

        
        if not zero_history:
            for tensor in self.arguments.keras_tensors:
                if not hasattr(tensor, "_keras_history"):
                    tensor._keras_history = KerasHistory(
                        operation=None, node_index=0, tensor_index=0
                    )

        
        self.operation._inbound_nodes.append(self)
        for kt in self.arguments.keras_tensors:
            inbound_op = kt._keras_history.operation
            if inbound_op is not None:  
                inbound_op._outbound_nodes.append(self)

        
        if not zero_history:
            node_index = len(self.operation._inbound_nodes) - 1
            for i, tensor in enumerate(self.outputs):
                tensor._keras_history = KerasHistory(
                    operation=operation, node_index=node_index, tensor_index=i
                )

        
        self.is_input = not self.arguments.keras_tensors

    def __repr__(self):
        return f"<Node operation={self.operation}, id={id(self)}>"

    @property
    def input_tensors(self):
        return self.arguments.keras_tensors

    @property
    def output_tensors(self):
        return self.outputs

    @property
    def parent_nodes(self):
        
        node_deps = []
        for kt in self.arguments.keras_tensors:
            op = kt._keras_history.operation
            node_index = kt._keras_history.node_index
            if op is not None:  
                node_deps.append(op._inbound_nodes[node_index])
        return node_deps


class KerasHistory(
    collections.namedtuple(
        "KerasHistory", ["operation", "node_index", "tensor_index"]
    )
):
    

    
    
    __slots__ = ()


def is_keras_tensor(obj):
    return hasattr(obj, "_keras_history")