hyperway


Namehyperway JSON
Version 1.0.4 PyPI version JSON
download
home_pageNone
SummaryA graph based functional execution library. Connect functions arbitrarily through a unique API
upload_time2025-10-21 21:29:03
maintainerNone
docs_urlNone
authorNone
requires_python>=3.8
licenseNone
keywords graph execution-engine functional-programming dataflow computational-graph workflow pipeline
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            
<div align="center">

<br>

![hyperway logo](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/logo-800.png)

<br>



[![Upload Python Package](https://github.com/Strangemother/python-hyperway/actions/workflows/python-publish.yml/badge.svg)](https://github.com/Strangemother/python-hyperway/actions/workflows/python-publish.yml)
[![PyPI](https://img.shields.io/pypi/v/hyperway?label=hyperway)](https://pypi.org/project/hyperway/)
![License](https://img.shields.io/pypi/l/hyperway)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/hyperway)](https://pypi.org/project/hyperway/)
[![Sonar Coverage](https://img.shields.io/sonar/coverage/Strangemother_python-hyperway?server=https%3A%2F%2Fsonarcloud.io)](https://sonarcloud.io/summary/new_code?id=Strangemother_python-hyperway)

# Hyperway

A Python graph based functional execution library, with a unique API.

</div>

+ [Graph](#graph)
+ [Stepper](#-stepper)
+ [Units (Nodes)](#units-nodes) | [Functions](#functions)
+ [Connections (Edges)](#connections-edges) | [Wire Functions](#wire-function)
+ [Reference Links](#areas-of-interest)

Hyperway is a graph based functional execution library, allowing you to connect functions arbitrarily through a unique API. Mimic a large range of programming paradigms such as procedural, parallel, or aspect-oriented-programming. Build B-trees or decision trees, circuitry or logic gates; all with a simple _wiring_ methodology.



## 📦 Install

Hyperway has no enforced dependencies. 

Install via pip:

```bash
pip install hyperway
```

If you want to render graphs, ensure [graphviz](https://graphviz.org/) is installed. This can be done through your own methods, or by installing the optional dependency:

```bash
pip install hyperway[graphviz]
```

## 🚀 Quick Example

For a quick example of the important parts, let's connect some functions and run the chain:

```python
import hyperway
from hyperway.tools import factory as f

# Store
g = hyperway.Graph()

# Connect
first_connection = g.add(f.add_10, f.add_20)
# More connections
many_connections = g.connect(f.add_30, f.add_1, f.add_2, f.add_3)
# Cross connect
compound_connection = g.add(many_connections[1].b, f.add_3)

# Prepare to run
stepper = g.stepper(first_connection.a, 10)

# Run a step
concurrent_row = stepper.step()
```

> [!NOTE]
> The stepper yields rows of `(Unit, ArgsPack)` tuples representing the next functions to execute.
> Continue calling `step()` until no rows remain. Final results accumulate in `stepper.stash`.


That's it! You're a graph engineer.


Render this graph (if [graphviz](https://graphviz.org/) is installed):

```python
g.write('intro-example', directory='renders', direction='LR')
```

![connection diagram](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/intro-example.gv.png)

# Getting Started

Hyperway aims to simplify graph based execution chains, allowing a developer to use functional graphs without managing the connections.

> [!TIP]
> **TL;DR:** The `Unit` (or node) is a function connected to other nodes through `Connections`. The `Stepper` walks the `Graph` of all connections.

### Glossary

The Hyperway API aims to simplify standard graph-theory terminology, by providing more intuitive names for common graph components. Developers and mathematicians can extend the base library with preferred terms, without changing the core functionality:

```python
class Vertex(Unit): 
    """Alias for Unit (Node)"""
    pass
```

Hyperway components, and their conventional siblings:

| Hyperway | Graph Theory | Description |
| --- | --- | --- |
| `Graph` | Graph, or Tree | A flat dictionary to hold all connections |
| `Unit` | Node, Point, or Vertex | A function on a graph, bound through edges |
| `Connection` | Edge, link, or Line | A connection between Units |
| `Stepper` | Walker, or Agent | A graph walking tool |


## Graph

The `Graph` is a fancy `defaultdict` of tuples, used to store connections:

```python
import hyperway

g = hyperway.Graph()
```

There are a few convenience methods, such as `add()` and `connect()`, but fundamentally it's a dictionary of connections.

All Connections are stored within a single `Graph` instance. We can consider the graph as a dictionary register of all associated connections.

```python
from hyperway.graph import Graph, add
from hyperway.nodes import as_units
from hyperway.tools import factory as f

g = Graph()
unit_a, unit_b = as_units(f.add_2, f.mul_2)

connection = add(g, unit_a, unit_b)
```

Under the hood, The graph is just a `defaultdict` and doesn't do much.

## Connections (Edges)

A `Connection` binds two functions (or `Unit` objects) together. It represents an edge between two nodes.

+ [Create](#create)
+ [Run](#run-a-connection)
+ [Pluck](#plucking)
+ [Wire Function](#wire-function)
+ [Self Reference](#self-reference)


A connection needs a minimum of two nodes:

![connection diagram of two nodes with an optional wire function](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/connection.png)

```python
from hyperway.edges import make_edge

c = make_edge(f.add_1, f.add_2)
# <Connection>
```

### Create

We can create a connection in two ways:

#### Hyperway

The `Graph` has an `add()` method to create a connection between two functions:

```python
import hyperway
from hyperway.tools import factory as f

g = hyperway.Graph()

connection = g.add(f.add_1, f.add_2)
# <Connection>
```

#### Functional

Alternatively we can use the `make_edge` function directly (without a graph):

```python
from hyperway.edges import make_edge
from hyperway.tools import factory as f
c = make_edge(f.add_1, f.add_2)
# <Connection>
```

### Run a Connection

We can _run_ a connection, calling the chain of two nodes. Generally a `Connection` isn't used outside a graph unless we're playing with it.

> [!IMPORTANT]
> A `Connection` has two processing steps due to a potential _wire_ function. Consider using `pluck()` to run both steps.

A standard call to a connection will run node `a` (the left side):

```python
# connection = make_edge(f.add_1, f.add_2)
>>> value_part_a = connection(1) # call A-side `add_1`
2.0
```

Then process the second part `b` (providing the value from the first call):

```python
>>> connection.process(value_part_a) # call B-side `add_2`
4.0
```

Alternatively use the `pluck()` method.

### Plucking

We can "pluck" a connection (like plucking a string) to run the functions with any arguments:

```python
from hyperway.tools import factory as f
from hyperway.edges import make_edge

c = make_edge(f.add_1, f.add_2)
# Run side _a_ (`add_1`) and _b_ (`add_2`) with our input value.

c.pluck(1)    # 4.0 == 1 + 1 + 2
c.pluck(10)   # 13.0 == 10 + 1 + 2
```

The `pluck()` executes both nodes and the optional wire function, in the expected order. Fundamentally a connection is self-contained and doesn't require a parent graph.


### Wire Function

An optional wire function exists between two nodes

![connection diagram of two nodes with a wire function](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/connection-with-wire.png)


The [Connection](#connections-edges) can have a function existing between its connected [Units](#units-nodes), allowing the alteration of the data through transit (whilst running through a connection):


```python
from hyperway.tools import factory as f
from hyperway.edges import make_edge, wire

c = make_edge(f.add_1, f.add_2, through=wire(f.mul_2))
# <Connection(Unit(func=P_add_1.0), Unit(func=P_add_2.0), through="P_mul_2.0" name=None)>
```

When using the connection side A (the `f.add_1` function), the wire function `wire(f.mul_2)` can inspect the values as they move to `f.add_2`.

It's important to note Hyperway is _[left-associative](#order-of-operation)_. The order of operation computes linearly:

```python
assert c.pluck(1) == 6   # (1 + 1) * 2 + 2 == 6
assert c.pluck(10) == 24 # (10 + 1) * 2 + 2 == 24
```

---

#### Why use a wire function?

It's easy to argue a wire function is a _node_, and you can implement the wire function without this connection tap.

+ Wire functions are optional: node to node connections are inherently not optional.
+ Removing a wire function does not remove the edge: Edges are persistent to the graph
+ Wire functions may be inert (e.g. just logging); Nodes cannot be inert as they must be bound to edges.

Fundamentally a wire function exists for topological clarity and may be ignored.

---

`make_edge` can accept the wire function. It receives the concurrent values transmitting through the attached edge:

```python
from hyperway.edges import make_edge
from hyperway.packer import argspack

import hyperway.tools as t
f = t.factory

def doubler(v, *a, **kw):
    # The wire function `through` _doubles_ the given number.
    # response with an argpack.
    return argspack(v * 2, **kw)


c = make_edge(f.add_1, f.add_2, through=doubler)
# <Connection(Unit(func=P_add_1.0), Unit(func=P_add_2.0), name=None)>

c.pluck(1)  # 6.0
c.pluck(2)  # 8.0
c.pluck(3)  # 10.0
```

> [!IMPORTANT]
> Wire functions **must** return an `ArgsPack` via `argspack()`. Returning raw values will break the execution chain.
> See [Extras](#extras) for `argspack` details.


The wire function is the reason for a two-step process when executing connections:

```python
# Call Node A: (+ 1)
c(4)
5.0

# Call Wire + B: (double then + 2)
c.process(5.0)
12.0
```

Or a single `pluck()`:

```python
c.pluck(4)
12.0
```

#### Self Reference

A connection `A -> B` may be the same node, performing a _loop_ or self-referencing node connection.

![self referencing connection](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/self-reference.png)


We can use the `as_unit` function, and reference the same unit on the graph:

```python
# pre-define the graph node wrapper
u = as_unit(f.add_2)
# Build an loop edge
e = make_edge(u, u)

g = Graph()
g.add_edge(e)

# Setup the start from the unit (side A)
g.stepper(u, 1)

# Call the stepper forever.
g.step()
g.step()
...
# 3, 5, 7, 9, 11, ...
```


## Functions

A function is our working code. We can borrow operator functions from the tools:

```python
from hyperway.tools import factory as f
add_10 = f.add_10
# functools.partial(<built-in function add>, 10.0)
add_10(1)
# 11.0
add_10(14)
# 24.0
```


## Units (Nodes)


The `Unit` (or node) is a function connected to other nodes through a `Connection`. A `Unit` is a wrapper for any python function. Everything in the graph (and in a Connection) is a `Unit`.


When we create a new connection, it automatically wraps the given functions as `Unit` types:

```python
c = make_edge(f.mul_3, f.add_4)
c.a
# <Unit(func=P_mul_3.0)>
c.b
# <Unit(func=P_add_4.0)>
```

A Unit has additional methods used by the graph tools, such as the `process` method:

```python
# Call our add_4 function:
>>> c.b.process(1)
5.0
```

A new unit is very unique. Creating a Connection with the same function for both sides `a` and `b`, will insert two new nodes:

```python
c = make_edge(f.add_4, f.add_4)
c.pluck(4)
12.0
```

We can cast a function as a `Unit` before insertion, allowing the re-reference to _existing_ nodes.

### Very Unique `Unit`

> [!IMPORTANT]
> A `Unit` is unique, even when using the same function:
> `Unit(my_func)` != `Unit(my_func)`


If you create a Unit using the same function, this will produce two unique units:

```python
unit_a = as_unit(f.add_2)
unit_a_2 = as_unit(f.add_2)
assert unit_a != unit_a_2  # Not the same.
```

Attempting to recast a `Unit`, will return the same `Unit`:

```python
unit_a = as_unit(f.add_2)
unit_a_2 = as_unit(unit_a) # unit_a is already a Unit
assert unit_a == unit_a_2  # They are the same
```

---

With this we create a linear chain of function calls, or close a loop that will run forever.

### Linear (not closed)

Generally when inserting functions, a new reference is created. This allows us to use the same function at different points in a chain:

![3 nodes linear chain](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/3-nodes.png)

```python
a = f.add_1
b = f.add_2
c = f.add_2

# a -> b -> c | done.
connection_1 = make_edge(a, b)
_ = make_edge(b, c)
_ = make_edge(c, a)

```


### Loop (closed)

Closing a path produces a loop. To close a path we can reuse the same `Unit` at both ends of our path.

![3 nodes loop](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/3-node-loop.png)

To ensure a node is reused when applied, we pre-convert it to a `Unit`:

```python
a = as_unit(f.add_1) # sticky reference.
b = f.add_2
c = f.add_2

# a -> b -> c -> a ... forever
connection_1 = make_edge(a, b)
_ = make_edge(b, c)
_ = make_edge(c, a)
```



## 👣 Stepper

The `Stepper` run units and discovers connections through the attached Graph. It runs concurrent units and spools the next callables for the next _step_.

![self referencing connection](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/stepper.png)

```python
from hyperway.graph import Graph
from hyperway.tools import factory as f

g = Graph()
a_connections = g.connect(f.add_10, f.add_20, f.add_30)
b_connections = g.connect(f.add_1, f.add_2, f.add_3)
c_connection = g.add(b_connections[1].b, f.add_3)

first_connection_first_node = a_connections[0].a
stepper = g.stepper(first_connection_first_node, 10)
# <stepper.StepperC object at 0x000000000258FEB0>

concurrent_row = stepper.step()
# rows. e.g: ((<Unit(func=my_func)>, <ArgsPack(*(1,), **{})>),)
```

For each `step()` call, we yield a step. When iterating from `first_connection_first_node` (`f.add_10`), the stepper will _pause_ half-way through our call. The _next_ step will resolve the value and prepare the next step:

```python
# From above:
# g.stepper(first_connection_first_node, 10)

stepper.step()
(
    # Partial edge (from add_10 to add_20), with the value "20.0" (10 add 10)
    (<edges.PartialConnection>, <ArgsPack(*(20.0,), **{})>),
)


stepper.step()
(
    # Previous step complete; input(10), add(10), then add(20)
    (<Unit(func=P_add_30.0)>, <ArgsPack(*(40.0,), **{})>),
)

```

We initiated a stepper at our preferred node `stepper = g.stepper(first_connection_first_node, 10)`. Any subsequent `stepper.step()` calls _push_ the stepper to the next execution step.

Each iteration returns the _next_ thing to perform and the values from the previous unit call.

```python
# Many (1) rows to call next.
(
    (<Unit(func=P_add_30.0)>, <ArgsPack(*(40.0,), **{})>),
)
```

We see one row, with `f.add_30` as the _next_ function to call.


### `run_stepper` Function

The stepper can run once (allowing us to loop it), or we can use the built-in `run_stepper` function, to walk the nodes until the chain is complete

```python
from hyperway.graph import Graph
from hyperway.tools import factory as f

from hyperway.packer import argspack
from hyperway.stepper import run_stepper


g = Graph()
connections = g.connect(f.add_10, f.add_20, f.add_30)

# run until exhausted
result = run_stepper(g, connections[0].a, argspack(10))
```

---

# 📊 Results

The value of the stepper is concurrent. When a path ends, the value is stored in the `stepper.stash`.
When executing node steps, the result from the call is given to the next connected unit.

![stepper classic path movement](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/stepper-classic-path.png)

If two nodes call to the same destination node, this causes _two_ calls of the next node:

```python
           +4
i  +2  +3       print
           +5
```

With this layout, the `print` function will be called twice by the `+4` and `+5` node. Two calls occur:

```python

          10
 1  3  6      print
          11

# Two results
print(10) # from path: 1 → +2 → +3 → +4 → 10
print(11) # from path: 1 → +2 → +3 → +5 → 11
```

This is because there are two connections _to_ the `print` node, causing two calls.


## Result Concatenation (Merge Nodes)

Use merge nodes to action _one_ call to a node, with two results.

1. Set `merge_node=True` on target node
2. Flag `concat_aware=True` on the stepper

![stepper merge node](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/stepper-merge.png)

```python
g = Graph()
u = as_unit(print)
u.merge_node = True

s = g.stepper()
s.concat_aware = True

s.step()
...
```

When processing a print merge-node, one call is executed when events occur through multiple connections during one step:

```python
         10
1  3  6      print
         11

print(10, 11) # resultant
```


## Results Explode!

> [!IMPORTANT]
> Every _fork_ within the graph will yield a new path.

A _path_ defines the flow of a `stepper` through a single processing chain. A function connected to more than one function will _fork_ the stepper and produce a result per connection.

![stepper classic path movement](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/stepper-value-2-fork-wires.png)

For example a graph with a a split path will yield two results:

```python
from hyperway import Graph, as_units
from hyperway.tools import factory as f

g = Graph()

split, join = as_units(f.add_2, f.add_2)

cs = g.connect(f.add_1, split)
g.connect(split, f.add_3, join)
g.connect(split, f.add_4, join)
g.connect(join, f.add_1)
```

If [graphviz](https://github.com/xflr6/graphviz) is installed, The graph can be rendered with `graph.write()`:

```python
# Continued from above
g.write('double-split', direction='LR')
```

![double split](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/double-split.gv.png)

---

Connecting nodes will grow the result count. For example creating in two exits nodes will double the result count

![stepper classic path movement](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/stepper-value-4-fork-wires.png)

To model this, we can extend the _above_ code with an extra connection: `g.connect(join, f.sub_1)`:

```python
# same as above
from hyperway import Graph, as_units
from hyperway.tools import factory as f

g = Graph()

split, join = as_units(f.add_2, f.add_2)

cs = g.connect(f.add_1, split)
g.connect(split, f.add_3, join)
g.connect(split, f.add_4, join)
g.connect(join, f.add_1)

# connect another function
g.connect(join, f.sub_1)

g.write('double-double-split', direction='LR')
```

![double split with two exit nodes](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/double-double-split.png)

> [!NOTE]
> The count of results is a _product_ of the node count - and may result exponential paths if unmanaged.
> Hyperway can **and will** execute this forever.

_Wire functions have been removed for clarity_

![stepper classic path movement](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/stepper-value-9-fork.png)

```python
from hyperway import Graph, as_unit

g = Graph()

split = as_unit(f.add_2, name='split')
join_a = as_unit(f.add_2)

# sum([1,2,3]) accepts an array - so we merge the args for the response.
join = as_unit(lambda *x: sum(x), name='sum')

cs = g.connect(f.add_1, split)
g.connect(split, f.add_3, join)
g.connect(split, f.add_4, join)
g.connect(split, f.add_5, join)

g.connect(join, f.add_1)
g.connect(join, f.sub_1)
g.connect(join, f.sub_2)
g.write('triple-split', direction='LR')
```

![triple split with three exit nodes](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/triple-split-3.gv.png)


#### Order of Operation

> [!IMPORTANT]
> Hyperway is left-associative, Therefore PEMDAS/BODMAS will not function as expected - graph chains execute linearly.

The order of precedence for operations occurs through sequential evaluation (from left to right) similar to C. Each operation is executed as it is encountered, without regard to the traditional precedence of operators.

+ [IBM z/OS Precedence and associativity][ibm-order-precedence]
+ [lagrammar.net][lag]
+ [Complexity in left-associative grammar][complexity-in-lag]


<table>
<thead><tr>
  <th align="left">Standard order precedence</th>
  <th align="left">Hyperway left-association</th>
</tr></thead>
<tbody><tr valign="top"><td>

```python
# BODMAS computes * first
 1 + 1 * 2 + 2 == 5
10 + 1 * 2 + 2 == 14
```

</td><td>

```python
# left-to-right computes linearly
( (1 + 1)  * 2) + 2 == 6
( (10 + 1) * 2) + 2 == 24
```

</td></tbody></table>

```python
# example sequential evaluation
from hyperway.tools import factory as f
from hyperway.edges import make_edge, wire

c = make_edge(f.add_1, f.add_2, through=wire(f.mul_2))

assert c.pluck(1) == 6   # (1 + 1) * 2 + 2 == 6
assert c.pluck(10) == 24 # (10 + 1) * 2 + 2 == 24
```

# Topology

![stepper classic path movement](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/stepper-value-fork.png)

+ [Graph](#graph-1): The Graph is a thin and dumb dictionary, maintaining a list of connections per node.
+ [Node](#units-and-nodes-1): The Node is also very terse, fundamentally acting as a thin wrapper around the user given function, and exposes a few methods for _on-graph_ executions.
+ [Edges](#connection-1): Edges or Connections are the primary focus of this version, where a single `Connection` is bound to two nodes, and maintains a wire-function.
+ [Stepper](#-stepper): The _Stepper_ performs most of the work for interacting with Nodes on a graph through Edges.

## Breakdown

We push _Node to Node_ Connections into the `Graph` dictionary. The Connection knows `A`, `B`, and potentially a Wire function.

When _running_ the graph we use a `Stepper` to processes each node step during iteration, collecting _results_ of each call, and the next executions to perform.

---

We _walk_ through the graph using a `Stepper`. Upon a step we call any rows of waiting callables. This may be the users first input and will yield _next callers_ and _the result_.

The `Stepper` should call each _next caller_ with the given _result_. Each _caller_ will return _next callers_ and a _result_ for the `Stepper` to call again.

In each iteration the callable resolves one or more connections. If no connections return for a node, The execution chain is considered complete.

### Understanding the Graph

The `Graph` is purposefully terse. Its build to be as minimal as possible for the task. In the raw solution the `Graph` is a `defaultdict(tuple)` with a few additional functions for node acquisition.

The graph maintains a list of `ID` to `Connection` set.

```python
{
  ID: (
        Connection(to=ID2),
    ),
  ID2: (
        Connection(to=ID),
    )
}
```

### Core Connection Knowledge

A `Connection` bind two functions and an optional wire function.

    A -> [W] -> B

When executing the connection, input starts through `A`, and returns through `B`. If the wire function exists it may alter the value before `B` receives its input values.

### Units and Nodes

A `Unit` represents a _thing_ on the graph, bound to other units through connections.

```python
def callable_func(value):
    return value * 3

as_unit(callable_func)
```

A unit is one reference

```python
unit = as_unit(callable_func)
unit2 = as_unit(callable_func)

assert unit != unit2
```

### Extras

#### `argspack`

The `argspack` simplifies the movement of arguments and keyword arguments for a function.

we can wrap the result as a pack, always ensuring its _unpackable_ when required.

```python
akw = argspack(100)
akw.a
(100, )

akw = argspack(foo=1)
akw.kw
{ 'foo': 1 }
```

---

# Project Goal

Although many existing libraries cater to graph theory, they often require a deep understanding of complex terminology and concepts. As an engineer without formal training in graph theory, these libraries are challenging.
**Hyperway** is the result of several years of research aimed at developing a simplified, functional, graph-based execution library with a minimal set of core features.


I'm slowly updating it to include the more advanced [future features](docs/future.md), such as hyper-edges and connection-decisions, bridging the gap between academic graph theory and practical application, providing developers with a low-level runtime that facilitates functional execution without the need for specialized knowledge.

# Areas of Interest

+ https://graphclasses.org/
+ https://houseofgraphs.org/
+ https://en.wikipedia.org/wiki/Cyber%E2%80%93physical_system
+ https://en.wikipedia.org/wiki/Signal-flow_graph
+ https://resources.wolframcloud.com/FunctionRepository/resources/HypergraphPlot
+ https://www.lancaster.ac.uk/stor-i-student-sites/katie-howgate/2021/04/29/hypergraphs-not-just-a-cool-name/

# Further Reading

+ [TensorFlow: Introduction to Graph and `tf.function`](https://www.tensorflow.org/guide/intro_to_graphs)
+ [Wiki: Extract, transform, load](https://en.wikipedia.org/wiki/Extract,_transform,_load)
+ https://docs.dgl.ai/en/2.0.x/notebooks/sparse/hgnn.html | https://github.com/iMoonLab/DeepHypergraph/blob/main/README.md
+ https://distill.pub/2021/gnn-intro/
+ https://renzoangles.net/gdm/

# Other Libraries

+ [Graphtik](https://graphtik.readthedocs.io/en/latest/)
+ [iGraph](https://python.igraph.org/en/stable/index.html#)
+ [graph-tool](https://graph-tool.skewed.de/)
+ [pyDot](https://github.com/pydot/pydot)
+ [FreExGraph](https://github.com/FreeYourSoul/FreExGraph)
+ [NetworkX](https://networkx.org/documentation/latest/index.html)

# Links

+ https://graphviz.org/

[ibm-order-precedence]: https://www.ibm.com/docs/en/zos/3.1.0?topic=section-precedence-associativity
[lag]: https://lagrammar.net/monographs/1999/slides/pdf/chapter-10.pdf
[complexity-in-lag]: https://www.researchgate.net/publication/222342088_Complexity_in_left-associative_grammar


            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "hyperway",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": null,
    "keywords": "graph, execution-engine, functional-programming, dataflow, computational-graph, workflow, pipeline",
    "author": null,
    "author_email": "Strangemother <hyperway@strangemother.com>",
    "download_url": "https://files.pythonhosted.org/packages/2a/2b/ca841cc4080cfc80ac9f8c1fff69451c801a3aeea2c46ab4cc9cf7178ffa/hyperway-1.0.4.tar.gz",
    "platform": null,
    "description": "\n<div align=\"center\">\n\n<br>\n\n![hyperway logo](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/logo-800.png)\n\n<br>\n\n\n\n[![Upload Python Package](https://github.com/Strangemother/python-hyperway/actions/workflows/python-publish.yml/badge.svg)](https://github.com/Strangemother/python-hyperway/actions/workflows/python-publish.yml)\n[![PyPI](https://img.shields.io/pypi/v/hyperway?label=hyperway)](https://pypi.org/project/hyperway/)\n![License](https://img.shields.io/pypi/l/hyperway)\n[![PyPI - Downloads](https://img.shields.io/pypi/dm/hyperway)](https://pypi.org/project/hyperway/)\n[![Sonar Coverage](https://img.shields.io/sonar/coverage/Strangemother_python-hyperway?server=https%3A%2F%2Fsonarcloud.io)](https://sonarcloud.io/summary/new_code?id=Strangemother_python-hyperway)\n\n# Hyperway\n\nA Python graph based functional execution library, with a unique API.\n\n</div>\n\n+ [Graph](#graph)\n+ [Stepper](#-stepper)\n+ [Units (Nodes)](#units-nodes) | [Functions](#functions)\n+ [Connections (Edges)](#connections-edges) | [Wire Functions](#wire-function)\n+ [Reference Links](#areas-of-interest)\n\nHyperway is a graph based functional execution library, allowing you to connect functions arbitrarily through a unique API. Mimic a large range of programming paradigms such as procedural, parallel, or aspect-oriented-programming. Build B-trees or decision trees, circuitry or logic gates; all with a simple _wiring_ methodology.\n\n\n\n## \ud83d\udce6 Install\n\nHyperway has no enforced dependencies. \n\nInstall via pip:\n\n```bash\npip install hyperway\n```\n\nIf you want to render graphs, ensure [graphviz](https://graphviz.org/) is installed. This can be done through your own methods, or by installing the optional dependency:\n\n```bash\npip install hyperway[graphviz]\n```\n\n## \ud83d\ude80 Quick Example\n\nFor a quick example of the important parts, let's connect some functions and run the chain:\n\n```python\nimport hyperway\nfrom hyperway.tools import factory as f\n\n# Store\ng = hyperway.Graph()\n\n# Connect\nfirst_connection = g.add(f.add_10, f.add_20)\n# More connections\nmany_connections = g.connect(f.add_30, f.add_1, f.add_2, f.add_3)\n# Cross connect\ncompound_connection = g.add(many_connections[1].b, f.add_3)\n\n# Prepare to run\nstepper = g.stepper(first_connection.a, 10)\n\n# Run a step\nconcurrent_row = stepper.step()\n```\n\n> [!NOTE]\n> The stepper yields rows of `(Unit, ArgsPack)` tuples representing the next functions to execute.\n> Continue calling `step()` until no rows remain. Final results accumulate in `stepper.stash`.\n\n\nThat's it! You're a graph engineer.\n\n\nRender this graph (if [graphviz](https://graphviz.org/) is installed):\n\n```python\ng.write('intro-example', directory='renders', direction='LR')\n```\n\n![connection diagram](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/intro-example.gv.png)\n\n# Getting Started\n\nHyperway aims to simplify graph based execution chains, allowing a developer to use functional graphs without managing the connections.\n\n> [!TIP]\n> **TL;DR:** The `Unit` (or node) is a function connected to other nodes through `Connections`. The `Stepper` walks the `Graph` of all connections.\n\n### Glossary\n\nThe Hyperway API aims to simplify standard graph-theory terminology, by providing more intuitive names for common graph components. Developers and mathematicians can extend the base library with preferred terms, without changing the core functionality:\n\n```python\nclass Vertex(Unit): \n    \"\"\"Alias for Unit (Node)\"\"\"\n    pass\n```\n\nHyperway components, and their conventional siblings:\n\n| Hyperway | Graph Theory | Description |\n| --- | --- | --- |\n| `Graph` | Graph, or Tree | A flat dictionary to hold all connections |\n| `Unit` | Node, Point, or Vertex | A function on a graph, bound through edges |\n| `Connection` | Edge, link, or Line | A connection between Units |\n| `Stepper` | Walker, or Agent | A graph walking tool |\n\n\n## Graph\n\nThe `Graph` is a fancy `defaultdict` of tuples, used to store connections:\n\n```python\nimport hyperway\n\ng = hyperway.Graph()\n```\n\nThere are a few convenience methods, such as `add()` and `connect()`, but fundamentally it's a dictionary of connections.\n\nAll Connections are stored within a single `Graph` instance. We can consider the graph as a dictionary register of all associated connections.\n\n```python\nfrom hyperway.graph import Graph, add\nfrom hyperway.nodes import as_units\nfrom hyperway.tools import factory as f\n\ng = Graph()\nunit_a, unit_b = as_units(f.add_2, f.mul_2)\n\nconnection = add(g, unit_a, unit_b)\n```\n\nUnder the hood, The graph is just a `defaultdict` and doesn't do much.\n\n## Connections (Edges)\n\nA `Connection` binds two functions (or `Unit` objects) together. It represents an edge between two nodes.\n\n+ [Create](#create)\n+ [Run](#run-a-connection)\n+ [Pluck](#plucking)\n+ [Wire Function](#wire-function)\n+ [Self Reference](#self-reference)\n\n\nA connection needs a minimum of two nodes:\n\n![connection diagram of two nodes with an optional wire function](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/connection.png)\n\n```python\nfrom hyperway.edges import make_edge\n\nc = make_edge(f.add_1, f.add_2)\n# <Connection>\n```\n\n### Create\n\nWe can create a connection in two ways:\n\n#### Hyperway\n\nThe `Graph` has an `add()` method to create a connection between two functions:\n\n```python\nimport hyperway\nfrom hyperway.tools import factory as f\n\ng = hyperway.Graph()\n\nconnection = g.add(f.add_1, f.add_2)\n# <Connection>\n```\n\n#### Functional\n\nAlternatively we can use the `make_edge` function directly (without a graph):\n\n```python\nfrom hyperway.edges import make_edge\nfrom hyperway.tools import factory as f\nc = make_edge(f.add_1, f.add_2)\n# <Connection>\n```\n\n### Run a Connection\n\nWe can _run_ a connection, calling the chain of two nodes. Generally a `Connection` isn't used outside a graph unless we're playing with it.\n\n> [!IMPORTANT]\n> A `Connection` has two processing steps due to a potential _wire_ function. Consider using `pluck()` to run both steps.\n\nA standard call to a connection will run node `a` (the left side):\n\n```python\n# connection = make_edge(f.add_1, f.add_2)\n>>> value_part_a = connection(1) # call A-side `add_1`\n2.0\n```\n\nThen process the second part `b` (providing the value from the first call):\n\n```python\n>>> connection.process(value_part_a) # call B-side `add_2`\n4.0\n```\n\nAlternatively use the `pluck()` method.\n\n### Plucking\n\nWe can \"pluck\" a connection (like plucking a string) to run the functions with any arguments:\n\n```python\nfrom hyperway.tools import factory as f\nfrom hyperway.edges import make_edge\n\nc = make_edge(f.add_1, f.add_2)\n# Run side _a_ (`add_1`) and _b_ (`add_2`) with our input value.\n\nc.pluck(1)    # 4.0 == 1 + 1 + 2\nc.pluck(10)   # 13.0 == 10 + 1 + 2\n```\n\nThe `pluck()` executes both nodes and the optional wire function, in the expected order. Fundamentally a connection is self-contained and doesn't require a parent graph.\n\n\n### Wire Function\n\nAn optional wire function exists between two nodes\n\n![connection diagram of two nodes with a wire function](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/connection-with-wire.png)\n\n\nThe [Connection](#connections-edges) can have a function existing between its connected [Units](#units-nodes), allowing the alteration of the data through transit (whilst running through a connection):\n\n\n```python\nfrom hyperway.tools import factory as f\nfrom hyperway.edges import make_edge, wire\n\nc = make_edge(f.add_1, f.add_2, through=wire(f.mul_2))\n# <Connection(Unit(func=P_add_1.0), Unit(func=P_add_2.0), through=\"P_mul_2.0\" name=None)>\n```\n\nWhen using the connection side A (the `f.add_1` function), the wire function `wire(f.mul_2)` can inspect the values as they move to `f.add_2`.\n\nIt's important to note Hyperway is _[left-associative](#order-of-operation)_. The order of operation computes linearly:\n\n```python\nassert c.pluck(1) == 6   # (1 + 1) * 2 + 2 == 6\nassert c.pluck(10) == 24 # (10 + 1) * 2 + 2 == 24\n```\n\n---\n\n#### Why use a wire function?\n\nIt's easy to argue a wire function is a _node_, and you can implement the wire function without this connection tap.\n\n+ Wire functions are optional: node to node connections are inherently not optional.\n+ Removing a wire function does not remove the edge: Edges are persistent to the graph\n+ Wire functions may be inert (e.g. just logging); Nodes cannot be inert as they must be bound to edges.\n\nFundamentally a wire function exists for topological clarity and may be ignored.\n\n---\n\n`make_edge` can accept the wire function. It receives the concurrent values transmitting through the attached edge:\n\n```python\nfrom hyperway.edges import make_edge\nfrom hyperway.packer import argspack\n\nimport hyperway.tools as t\nf = t.factory\n\ndef doubler(v, *a, **kw):\n    # The wire function `through` _doubles_ the given number.\n    # response with an argpack.\n    return argspack(v * 2, **kw)\n\n\nc = make_edge(f.add_1, f.add_2, through=doubler)\n# <Connection(Unit(func=P_add_1.0), Unit(func=P_add_2.0), name=None)>\n\nc.pluck(1)  # 6.0\nc.pluck(2)  # 8.0\nc.pluck(3)  # 10.0\n```\n\n> [!IMPORTANT]\n> Wire functions **must** return an `ArgsPack` via `argspack()`. Returning raw values will break the execution chain.\n> See [Extras](#extras) for `argspack` details.\n\n\nThe wire function is the reason for a two-step process when executing connections:\n\n```python\n# Call Node A: (+ 1)\nc(4)\n5.0\n\n# Call Wire + B: (double then + 2)\nc.process(5.0)\n12.0\n```\n\nOr a single `pluck()`:\n\n```python\nc.pluck(4)\n12.0\n```\n\n#### Self Reference\n\nA connection `A -> B` may be the same node, performing a _loop_ or self-referencing node connection.\n\n![self referencing connection](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/self-reference.png)\n\n\nWe can use the `as_unit` function, and reference the same unit on the graph:\n\n```python\n# pre-define the graph node wrapper\nu = as_unit(f.add_2)\n# Build an loop edge\ne = make_edge(u, u)\n\ng = Graph()\ng.add_edge(e)\n\n# Setup the start from the unit (side A)\ng.stepper(u, 1)\n\n# Call the stepper forever.\ng.step()\ng.step()\n...\n# 3, 5, 7, 9, 11, ...\n```\n\n\n## Functions\n\nA function is our working code. We can borrow operator functions from the tools:\n\n```python\nfrom hyperway.tools import factory as f\nadd_10 = f.add_10\n# functools.partial(<built-in function add>, 10.0)\nadd_10(1)\n# 11.0\nadd_10(14)\n# 24.0\n```\n\n\n## Units (Nodes)\n\n\nThe `Unit` (or node) is a function connected to other nodes through a `Connection`. A `Unit` is a wrapper for any python function. Everything in the graph (and in a Connection) is a `Unit`.\n\n\nWhen we create a new connection, it automatically wraps the given functions as `Unit` types:\n\n```python\nc = make_edge(f.mul_3, f.add_4)\nc.a\n# <Unit(func=P_mul_3.0)>\nc.b\n# <Unit(func=P_add_4.0)>\n```\n\nA Unit has additional methods used by the graph tools, such as the `process` method:\n\n```python\n# Call our add_4 function:\n>>> c.b.process(1)\n5.0\n```\n\nA new unit is very unique. Creating a Connection with the same function for both sides `a` and `b`, will insert two new nodes:\n\n```python\nc = make_edge(f.add_4, f.add_4)\nc.pluck(4)\n12.0\n```\n\nWe can cast a function as a `Unit` before insertion, allowing the re-reference to _existing_ nodes.\n\n### Very Unique `Unit`\n\n> [!IMPORTANT]\n> A `Unit` is unique, even when using the same function:\n> `Unit(my_func)` != `Unit(my_func)`\n\n\nIf you create a Unit using the same function, this will produce two unique units:\n\n```python\nunit_a = as_unit(f.add_2)\nunit_a_2 = as_unit(f.add_2)\nassert unit_a != unit_a_2  # Not the same.\n```\n\nAttempting to recast a `Unit`, will return the same `Unit`:\n\n```python\nunit_a = as_unit(f.add_2)\nunit_a_2 = as_unit(unit_a) # unit_a is already a Unit\nassert unit_a == unit_a_2  # They are the same\n```\n\n---\n\nWith this we create a linear chain of function calls, or close a loop that will run forever.\n\n### Linear (not closed)\n\nGenerally when inserting functions, a new reference is created. This allows us to use the same function at different points in a chain:\n\n![3 nodes linear chain](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/3-nodes.png)\n\n```python\na = f.add_1\nb = f.add_2\nc = f.add_2\n\n# a -> b -> c | done.\nconnection_1 = make_edge(a, b)\n_ = make_edge(b, c)\n_ = make_edge(c, a)\n\n```\n\n\n### Loop (closed)\n\nClosing a path produces a loop. To close a path we can reuse the same `Unit` at both ends of our path.\n\n![3 nodes loop](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/3-node-loop.png)\n\nTo ensure a node is reused when applied, we pre-convert it to a `Unit`:\n\n```python\na = as_unit(f.add_1) # sticky reference.\nb = f.add_2\nc = f.add_2\n\n# a -> b -> c -> a ... forever\nconnection_1 = make_edge(a, b)\n_ = make_edge(b, c)\n_ = make_edge(c, a)\n```\n\n\n\n## \ud83d\udc63 Stepper\n\nThe `Stepper` run units and discovers connections through the attached Graph. It runs concurrent units and spools the next callables for the next _step_.\n\n![self referencing connection](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/stepper.png)\n\n```python\nfrom hyperway.graph import Graph\nfrom hyperway.tools import factory as f\n\ng = Graph()\na_connections = g.connect(f.add_10, f.add_20, f.add_30)\nb_connections = g.connect(f.add_1, f.add_2, f.add_3)\nc_connection = g.add(b_connections[1].b, f.add_3)\n\nfirst_connection_first_node = a_connections[0].a\nstepper = g.stepper(first_connection_first_node, 10)\n# <stepper.StepperC object at 0x000000000258FEB0>\n\nconcurrent_row = stepper.step()\n# rows. e.g: ((<Unit(func=my_func)>, <ArgsPack(*(1,), **{})>),)\n```\n\nFor each `step()` call, we yield a step. When iterating from `first_connection_first_node` (`f.add_10`), the stepper will _pause_ half-way through our call. The _next_ step will resolve the value and prepare the next step:\n\n```python\n# From above:\n# g.stepper(first_connection_first_node, 10)\n\nstepper.step()\n(\n    # Partial edge (from add_10 to add_20), with the value \"20.0\" (10 add 10)\n    (<edges.PartialConnection>, <ArgsPack(*(20.0,), **{})>),\n)\n\n\nstepper.step()\n(\n    # Previous step complete; input(10), add(10), then add(20)\n    (<Unit(func=P_add_30.0)>, <ArgsPack(*(40.0,), **{})>),\n)\n\n```\n\nWe initiated a stepper at our preferred node `stepper = g.stepper(first_connection_first_node, 10)`. Any subsequent `stepper.step()` calls _push_ the stepper to the next execution step.\n\nEach iteration returns the _next_ thing to perform and the values from the previous unit call.\n\n```python\n# Many (1) rows to call next.\n(\n    (<Unit(func=P_add_30.0)>, <ArgsPack(*(40.0,), **{})>),\n)\n```\n\nWe see one row, with `f.add_30` as the _next_ function to call.\n\n\n### `run_stepper` Function\n\nThe stepper can run once (allowing us to loop it), or we can use the built-in `run_stepper` function, to walk the nodes until the chain is complete\n\n```python\nfrom hyperway.graph import Graph\nfrom hyperway.tools import factory as f\n\nfrom hyperway.packer import argspack\nfrom hyperway.stepper import run_stepper\n\n\ng = Graph()\nconnections = g.connect(f.add_10, f.add_20, f.add_30)\n\n# run until exhausted\nresult = run_stepper(g, connections[0].a, argspack(10))\n```\n\n---\n\n# \ud83d\udcca Results\n\nThe value of the stepper is concurrent. When a path ends, the value is stored in the `stepper.stash`.\nWhen executing node steps, the result from the call is given to the next connected unit.\n\n![stepper classic path movement](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/stepper-classic-path.png)\n\nIf two nodes call to the same destination node, this causes _two_ calls of the next node:\n\n```python\n           +4\ni  +2  +3       print\n           +5\n```\n\nWith this layout, the `print` function will be called twice by the `+4` and `+5` node. Two calls occur:\n\n```python\n\n          10\n 1  3  6      print\n          11\n\n# Two results\nprint(10) # from path: 1 \u2192 +2 \u2192 +3 \u2192 +4 \u2192 10\nprint(11) # from path: 1 \u2192 +2 \u2192 +3 \u2192 +5 \u2192 11\n```\n\nThis is because there are two connections _to_ the `print` node, causing two calls.\n\n\n## Result Concatenation (Merge Nodes)\n\nUse merge nodes to action _one_ call to a node, with two results.\n\n1. Set `merge_node=True` on target node\n2. Flag `concat_aware=True` on the stepper\n\n![stepper merge node](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/stepper-merge.png)\n\n```python\ng = Graph()\nu = as_unit(print)\nu.merge_node = True\n\ns = g.stepper()\ns.concat_aware = True\n\ns.step()\n...\n```\n\nWhen processing a print merge-node, one call is executed when events occur through multiple connections during one step:\n\n```python\n         10\n1  3  6      print\n         11\n\nprint(10, 11) # resultant\n```\n\n\n## Results Explode!\n\n> [!IMPORTANT]\n> Every _fork_ within the graph will yield a new path.\n\nA _path_ defines the flow of a `stepper` through a single processing chain. A function connected to more than one function will _fork_ the stepper and produce a result per connection.\n\n![stepper classic path movement](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/stepper-value-2-fork-wires.png)\n\nFor example a graph with a a split path will yield two results:\n\n```python\nfrom hyperway import Graph, as_units\nfrom hyperway.tools import factory as f\n\ng = Graph()\n\nsplit, join = as_units(f.add_2, f.add_2)\n\ncs = g.connect(f.add_1, split)\ng.connect(split, f.add_3, join)\ng.connect(split, f.add_4, join)\ng.connect(join, f.add_1)\n```\n\nIf [graphviz](https://github.com/xflr6/graphviz) is installed, The graph can be rendered with `graph.write()`:\n\n```python\n# Continued from above\ng.write('double-split', direction='LR')\n```\n\n![double split](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/double-split.gv.png)\n\n---\n\nConnecting nodes will grow the result count. For example creating in two exits nodes will double the result count\n\n![stepper classic path movement](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/stepper-value-4-fork-wires.png)\n\nTo model this, we can extend the _above_ code with an extra connection: `g.connect(join, f.sub_1)`:\n\n```python\n# same as above\nfrom hyperway import Graph, as_units\nfrom hyperway.tools import factory as f\n\ng = Graph()\n\nsplit, join = as_units(f.add_2, f.add_2)\n\ncs = g.connect(f.add_1, split)\ng.connect(split, f.add_3, join)\ng.connect(split, f.add_4, join)\ng.connect(join, f.add_1)\n\n# connect another function\ng.connect(join, f.sub_1)\n\ng.write('double-double-split', direction='LR')\n```\n\n![double split with two exit nodes](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/double-double-split.png)\n\n> [!NOTE]\n> The count of results is a _product_ of the node count - and may result exponential paths if unmanaged.\n> Hyperway can **and will** execute this forever.\n\n_Wire functions have been removed for clarity_\n\n![stepper classic path movement](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/stepper-value-9-fork.png)\n\n```python\nfrom hyperway import Graph, as_unit\n\ng = Graph()\n\nsplit = as_unit(f.add_2, name='split')\njoin_a = as_unit(f.add_2)\n\n# sum([1,2,3]) accepts an array - so we merge the args for the response.\njoin = as_unit(lambda *x: sum(x), name='sum')\n\ncs = g.connect(f.add_1, split)\ng.connect(split, f.add_3, join)\ng.connect(split, f.add_4, join)\ng.connect(split, f.add_5, join)\n\ng.connect(join, f.add_1)\ng.connect(join, f.sub_1)\ng.connect(join, f.sub_2)\ng.write('triple-split', direction='LR')\n```\n\n![triple split with three exit nodes](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/triple-split-3.gv.png)\n\n\n#### Order of Operation\n\n> [!IMPORTANT]\n> Hyperway is left-associative, Therefore PEMDAS/BODMAS will not function as expected - graph chains execute linearly.\n\nThe order of precedence for operations occurs through sequential evaluation (from left to right) similar to C. Each operation is executed as it is encountered, without regard to the traditional precedence of operators.\n\n+ [IBM z/OS Precedence and associativity][ibm-order-precedence]\n+ [lagrammar.net][lag]\n+ [Complexity in left-associative grammar][complexity-in-lag]\n\n\n<table>\n<thead><tr>\n  <th align=\"left\">Standard order precedence</th>\n  <th align=\"left\">Hyperway left-association</th>\n</tr></thead>\n<tbody><tr valign=\"top\"><td>\n\n```python\n# BODMAS computes * first\n 1 + 1 * 2 + 2 == 5\n10 + 1 * 2 + 2 == 14\n```\n\n</td><td>\n\n```python\n# left-to-right computes linearly\n( (1 + 1)  * 2) + 2 == 6\n( (10 + 1) * 2) + 2 == 24\n```\n\n</td></tbody></table>\n\n```python\n# example sequential evaluation\nfrom hyperway.tools import factory as f\nfrom hyperway.edges import make_edge, wire\n\nc = make_edge(f.add_1, f.add_2, through=wire(f.mul_2))\n\nassert c.pluck(1) == 6   # (1 + 1) * 2 + 2 == 6\nassert c.pluck(10) == 24 # (10 + 1) * 2 + 2 == 24\n```\n\n# Topology\n\n![stepper classic path movement](https://raw.githubusercontent.com/Strangemother/python-hyperway/main/docs/images/stepper-value-fork.png)\n\n+ [Graph](#graph-1): The Graph is a thin and dumb dictionary, maintaining a list of connections per node.\n+ [Node](#units-and-nodes-1): The Node is also very terse, fundamentally acting as a thin wrapper around the user given function, and exposes a few methods for _on-graph_ executions.\n+ [Edges](#connection-1): Edges or Connections are the primary focus of this version, where a single `Connection` is bound to two nodes, and maintains a wire-function.\n+ [Stepper](#-stepper): The _Stepper_ performs most of the work for interacting with Nodes on a graph through Edges.\n\n## Breakdown\n\nWe push _Node to Node_ Connections into the `Graph` dictionary. The Connection knows `A`, `B`, and potentially a Wire function.\n\nWhen _running_ the graph we use a `Stepper` to processes each node step during iteration, collecting _results_ of each call, and the next executions to perform.\n\n---\n\nWe _walk_ through the graph using a `Stepper`. Upon a step we call any rows of waiting callables. This may be the users first input and will yield _next callers_ and _the result_.\n\nThe `Stepper` should call each _next caller_ with the given _result_. Each _caller_ will return _next callers_ and a _result_ for the `Stepper` to call again.\n\nIn each iteration the callable resolves one or more connections. If no connections return for a node, The execution chain is considered complete.\n\n### Understanding the Graph\n\nThe `Graph` is purposefully terse. Its build to be as minimal as possible for the task. In the raw solution the `Graph` is a `defaultdict(tuple)` with a few additional functions for node acquisition.\n\nThe graph maintains a list of `ID` to `Connection` set.\n\n```python\n{\n  ID: (\n        Connection(to=ID2),\n    ),\n  ID2: (\n        Connection(to=ID),\n    )\n}\n```\n\n### Core Connection Knowledge\n\nA `Connection` bind two functions and an optional wire function.\n\n    A -> [W] -> B\n\nWhen executing the connection, input starts through `A`, and returns through `B`. If the wire function exists it may alter the value before `B` receives its input values.\n\n### Units and Nodes\n\nA `Unit` represents a _thing_ on the graph, bound to other units through connections.\n\n```python\ndef callable_func(value):\n    return value * 3\n\nas_unit(callable_func)\n```\n\nA unit is one reference\n\n```python\nunit = as_unit(callable_func)\nunit2 = as_unit(callable_func)\n\nassert unit != unit2\n```\n\n### Extras\n\n#### `argspack`\n\nThe `argspack` simplifies the movement of arguments and keyword arguments for a function.\n\nwe can wrap the result as a pack, always ensuring its _unpackable_ when required.\n\n```python\nakw = argspack(100)\nakw.a\n(100, )\n\nakw = argspack(foo=1)\nakw.kw\n{ 'foo': 1 }\n```\n\n---\n\n# Project Goal\n\nAlthough many existing libraries cater to graph theory, they often require a deep understanding of complex terminology and concepts. As an engineer without formal training in graph theory, these libraries are challenging.\n**Hyperway** is the result of several years of research aimed at developing a simplified, functional, graph-based execution library with a minimal set of core features.\n\n\nI'm slowly updating it to include the more advanced [future features](docs/future.md), such as hyper-edges and connection-decisions, bridging the gap between academic graph theory and practical application, providing developers with a low-level runtime that facilitates functional execution without the need for specialized knowledge.\n\n# Areas of Interest\n\n+ https://graphclasses.org/\n+ https://houseofgraphs.org/\n+ https://en.wikipedia.org/wiki/Cyber%E2%80%93physical_system\n+ https://en.wikipedia.org/wiki/Signal-flow_graph\n+ https://resources.wolframcloud.com/FunctionRepository/resources/HypergraphPlot\n+ https://www.lancaster.ac.uk/stor-i-student-sites/katie-howgate/2021/04/29/hypergraphs-not-just-a-cool-name/\n\n# Further Reading\n\n+ [TensorFlow: Introduction to Graph and `tf.function`](https://www.tensorflow.org/guide/intro_to_graphs)\n+ [Wiki: Extract, transform, load](https://en.wikipedia.org/wiki/Extract,_transform,_load)\n+ https://docs.dgl.ai/en/2.0.x/notebooks/sparse/hgnn.html | https://github.com/iMoonLab/DeepHypergraph/blob/main/README.md\n+ https://distill.pub/2021/gnn-intro/\n+ https://renzoangles.net/gdm/\n\n# Other Libraries\n\n+ [Graphtik](https://graphtik.readthedocs.io/en/latest/)\n+ [iGraph](https://python.igraph.org/en/stable/index.html#)\n+ [graph-tool](https://graph-tool.skewed.de/)\n+ [pyDot](https://github.com/pydot/pydot)\n+ [FreExGraph](https://github.com/FreeYourSoul/FreExGraph)\n+ [NetworkX](https://networkx.org/documentation/latest/index.html)\n\n# Links\n\n+ https://graphviz.org/\n\n[ibm-order-precedence]: https://www.ibm.com/docs/en/zos/3.1.0?topic=section-precedence-associativity\n[lag]: https://lagrammar.net/monographs/1999/slides/pdf/chapter-10.pdf\n[complexity-in-lag]: https://www.researchgate.net/publication/222342088_Complexity_in_left-associative_grammar\n\n",
    "bugtrack_url": null,
    "license": null,
    "summary": "A graph based functional execution library. Connect functions arbitrarily through a unique API",
    "version": "1.0.4",
    "project_urls": {
        "Bug Tracker": "https://github.com/Strangemother/python-hyperway/issues",
        "Changelog": "https://github.com/Strangemother/python-hyperway/blob/main/RELEASE_NOTES_v1.0.0.md",
        "Documentation": "https://github.com/Strangemother/python-hyperway/tree/main/docs",
        "Homepage": "https://github.com/Strangemother/python-hyperway",
        "Source Code": "https://github.com/Strangemother/python-hyperway"
    },
    "split_keywords": [
        "graph",
        " execution-engine",
        " functional-programming",
        " dataflow",
        " computational-graph",
        " workflow",
        " pipeline"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "512fd8d1aae23788bfe3950c623a267e23f406401ef40bd3affb27601a14c455",
                "md5": "5cc63aa6e9123908db035806d5200289",
                "sha256": "d3c9048e65d5559083e20c208534bcaa64179d6f13a845fa2427694289fa8f76"
            },
            "downloads": -1,
            "filename": "hyperway-1.0.4-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "5cc63aa6e9123908db035806d5200289",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 27897,
            "upload_time": "2025-10-21T21:29:02",
            "upload_time_iso_8601": "2025-10-21T21:29:02.301375Z",
            "url": "https://files.pythonhosted.org/packages/51/2f/d8d1aae23788bfe3950c623a267e23f406401ef40bd3affb27601a14c455/hyperway-1.0.4-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "2a2bca841cc4080cfc80ac9f8c1fff69451c801a3aeea2c46ab4cc9cf7178ffa",
                "md5": "d7a4606cfd0279160520dd71b14139ac",
                "sha256": "2fc20ed7946191505d55e61fde457f9ea65c140f3309fe44ead5ecf121a2ad00"
            },
            "downloads": -1,
            "filename": "hyperway-1.0.4.tar.gz",
            "has_sig": false,
            "md5_digest": "d7a4606cfd0279160520dd71b14139ac",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 70000,
            "upload_time": "2025-10-21T21:29:03",
            "upload_time_iso_8601": "2025-10-21T21:29:03.613304Z",
            "url": "https://files.pythonhosted.org/packages/2a/2b/ca841cc4080cfc80ac9f8c1fff69451c801a3aeea2c46ab4cc9cf7178ffa/hyperway-1.0.4.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-10-21 21:29:03",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "Strangemother",
    "github_project": "python-hyperway",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "tox": true,
    "lcname": "hyperway"
}
        
Elapsed time: 1.55780s