Metadata-Version: 2.1
Name: lazy_graph
Version: 0.3.14
Summary: lazy graph framework
Author-email: Hao Zhang <zh970205@mail.ustc.edu.cn>
License: GPLv3
Project-URL: Homepage, https://github.com/USTC-TNS/TNSP/tree/main/lazy_graph
Project-URL: Repository, https://github.com/USTC-TNS/TNSP.git
Project-URL: Issues, https://github.com/USTC-TNS/TNSP/issues
Project-URL: Changelog, https://github.com/USTC-TNS/TNSP/blob/main/CHANGELOG.org
Keywords: framework,lazy evaluation,computational graph
Classifier: Development Status :: 4 - Beta
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
Classifier: Topic :: Scientific/Engineering :: Mathematics
Classifier: Topic :: Scientific/Engineering :: Physics
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.7
Description-Content-Type: text/markdown

The lazy-graph is a Python library designed to facilitate lazy evaluation,
offering additional functionality for updating upstream values
and allowing easy duplication of the entire lazy graph structure.


# Install

Please either copy or create a soft link for the directory in the `site-packages` directory.
Alternatively, you can utilize pip to install the lazy-graph package by running the command `pip install lazy_graph`.


# Documents


## A simple example

We can create a root node `a` with the value `1` and another root node `b` with the value `2`,
followed by creating a new node called `c`, where its value is equal to the sum of the values of nodes `a` and `b`.
The function for adding these nodes will be triggered whenever we attempt to retrieve the value of node `c`.

    from lazy import Root, Node
    
    
    def add(a, b):
        print(f"calculating {a} + {b}")
        return a + b
    
    
    print("create nodes")
    a = Root(1)
    b = Root(2)
    c = Node(add, a, b)
    print("get the value")
    print(f"c is {c()}")

    create nodes
    get the value
    calculating 1 + 2
    c is 3

As demonstrated earlier, to create a root node containing the value of `x`, we use the expression `Root(x)`.
On the other hand, to obtain a node with its value determined by the function `func` along with any additional arguments or keyword arguments (if provided),
you would call `Node(func, *args, **kwargs)`.
This will generate a node whose value is computed as `func(*args, **kwargs)`.

To obtain the value of a given node `n`, simply use the function `n()`.
This calculates the value for the initial run and then utilizes caching for subsequent calls, ensuring efficiency in your code.


## Check if a node has already been computed

To determine if the value of a specified node `n` has already been computed and stored in cache,
you can utilize `bool(n)`.
This function returns `True` if the node's value exists in the cache, and `False` otherwise.

    a = Root(1)
    b = Root(2)
    c = Node(add, a, b)
    print(bool(c))
    print("c is", c())
    print(bool(c))

    False
    calculating 1 + 2
    c is 3
    True


## An example of updating an upstream node

    print("create nodes")
    a = Root(1)
    b = Root(2)
    c = Node(add, a, b)
    print("get the value")
    print(f"c is {c()}")
    print("get the value again")
    print(f"c is {c()}")
    print("update upstream")
    a.reset(4)
    print("get the new value")
    print(f"c is {c()}")

    create nodes
    get the value
    calculating 1 + 2
    c is 3
    get the value again
    c is 3
    update upstream
    get the new value
    calculating 4 + 2
    c is 6

In the provided code snippet, prior to resetting the upstream node `a`,
the value of node `c` is computed only once during its initial execution and subsequently utilizes a cache mechanism for subsequent calls.
Then, by calling `a.reset(v)`, where `v` equals `4` here, the value of node `a` can be reset to this new value.
After this operation, invoking `c()` will cause the function to be executed once more in order to obtain the updated value of node `c`.


## Both positional and keyword arguments are accepted, as well as regular values and lazy nodes

Both positional and keyword arguments are supported, allowing for a flexible approach when creating nodes.
You can mix these arguments with regular values as needed.
In the example provided, we utilize various types of arguments,
such as positional regular values, positional lazy nodes, keyword regular values, and keyword lazy nodes,
to construct node `z`.

    def add4(a, b, c, d):
        print(f"calculating {a} + {b} + {c} + {d}")
        return a + b + c + d
    
    
    print("create nodes")
    a = Root(1)
    c = Root(3)
    z = Node(add4, a, 2, c=c, d=4)
    print("get the value")
    print(f"c is {z()}")

    create nodes
    get the value
    calculating 1 + 2 + 3 + 4
    c is 10


## Copy the graph of lazy nodes

    from lazy import Copy
    
    print("create nodes")
    a = Root(1)
    b = Root(2)
    c = Node(add, a, b)
    print("get the value")
    print(f"c is {c()}")
    
    print("copy lazy graph")
    copy = Copy()
    new_a = copy(a)
    new_b = copy(b)
    new_c = copy(c)
    
    print("get the new value")
    print(f"new c is {new_c()}")

    create nodes
    get the value
    calculating 1 + 2
    c is 3
    copy lazy graph
    get the new value
    new c is 3

In addition to the previously simple example, we duplicate the graph,
copying `a` to `new_a`, `b` to `new_b`, and `c` to `new_c`.
This is done using a copy handle acquired through the \`Copy()\` function.
Once you have obtained the handle with `copy = Copy()`,
you can then utilize `copy(old_node)` to obtain the corresponding `new_node`.

After copying the graph, the cache is also reused whenever possible.
For instance, the `add` function isn't called when retrieving the value of node `new_c`.

    print("reset value")
    a.reset(4)
    new_a.reset(8)
    print("get the old value and new value")
    print(f"c is {c()}, new c is {new_c()}")

    reset value
    get the old value and new value
    calculating 4 + 2
    calculating 8 + 2
    c is 6, new c is 10

In the copied graph, the relationships between nodes are identical to those in the original graph,
along with the cache when feasible.
However, resetting the value of a node in one graph does not impact any other graphs.

In some cases, users might wish to duplicate just a portion of an entire graph.
In such instances, both graphs will share the same upstream nodes for those that haven't been replicated.
For instance, consider the example below where node `a` is shared between the two graphs.
However, the second graph contains unique nodes `new_b` and `new_c`, which correspond to `a` and `b` respectively in the initial graph.

    copy = Copy()
    new_b = copy(b)
    new_c = copy(c)
    
    print(f"a is {a()}")
    print(f"b is {b()}, new b is {new_b()}")
    print(f"c is {c()}, new c is {new_c()}")
    b.reset(8)
    print(f"c is {c()}, new c is {new_c()}")
    new_b.reset(10)
    print(f"c is {c()}, new c is {new_c()}")
    a.reset(6)
    print(f"c is {c()}, new c is {new_c()}")

    a is 4
    b is 2, new b is 2
    c is 6, new c is 6
    calculating 4 + 8
    c is 12, new c is 6
    calculating 4 + 10
    c is 12, new c is 14
    calculating 6 + 8
    calculating 6 + 10
    c is 14, new c is 16

In order to prevent misuse, if a user attempts to duplicate the same node multiple times, the copy handler will provide the same new node each time.

    new_c = copy(c)
    new_c_2 = copy(c)
    print(id(new_c) == id(new_c_2))

    True

When duplicating a lazy graph,
it is essential to replicate the upstream nodes prior to proceeding with the downstream nodes.
This guarantees that the package can effectively handle the dependencies among the various nodes of the graph.

