Skip to content

Commit

Permalink
Merge pull request #342 from Point72/document-delayed-edge-run-on-thread
Browse files Browse the repository at this point in the history
Improve `csp.feedback` documentation with better example and document `DelayedEdge`
  • Loading branch information
timkpaine authored Jul 19, 2024
2 parents 4a0a80c + 3d94b74 commit 3333348
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 53 deletions.
2 changes: 1 addition & 1 deletion docs/wiki/_Sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ Notes for editors:
- [Historical Buffers](Historical-Buffers)
- [Execution Modes](Execution-Modes)
- [Adapters](Adapters)
- [Feedback and Delayed Edge](Feedback-and-Delayed-Edge)
- [Common Mistakes](Common-Mistakes)

**How-to guides**

- [Use Statistical Nodes](Use-Statistical-Nodes)
- Use Adapters (coming soon)
- [Add Cycles in Graphs](Add-Cycles-in-Graphs)
- [Create Dynamic Baskets](Create-Dynamic-Baskets)
- Write Adapters:
- [Write Historical Input Adapters](Write-Historical-Input-Adapters)
Expand Down
97 changes: 97 additions & 0 deletions docs/wiki/concepts/Feedback-and-Delayed-Edge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
## Table of Contents

- [Table of Contents](#table-of-contents)
- [`csp.feedback`](#cspfeedback)
- [`csp.DelayedEdge`](#cspdelayededge)

CSP has two methods of late binding a time series to an edge. The first, `csp.feedback`, is used to create connections from downstream nodes to upstream nodes in a graph without introducing a cycle.
The second, `csp.DelayedEdge`, is used when a single edge may come from many possible sources and will be bound after its passed as an argument to some other node.

## `csp.feedback`

CSP graphs are always *directed acyclic graphs* (DAGs); however, there are some occasions where you may want to feed back the output of a downstream node into a node at a prior [rank](CSP-Graph#Graph-Propagation-and-Single-Dispatch).

This is usually the case when a node is making a decision that depends on the result of its previous action.
For example, consider a `csp.graph` that simulates population dynamics. We have a system of [wolves and elk](https://www.yellowstonepark.com/things-to-do/wildlife/wolf-reintroduction-changes-ecosystem/) with W wolves and E elk.
We have a node `change_wolf_pop` which simulates introducing or removing wolves from the ecosystem. Then we have a node `compute_elk_pop` which computes the new expected elk population based on the number of wolves *and* other factors.
The output of `compute_elk_pop`, being the number of expected elks, will **feed back** into the `change_wolf_pop` node. This means the population dynamics model can be wholly separate from our intervention decision.

For use cases like this, the `csp.feedback` construct exists. It allows us to pass the output of a downstream node to one upstream so that a recomputation is triggered on the next [engine cycle](CSP-Graph#Graph-Propagation-and-Single-Dispatch).
Using `csp.feedback`, one can wire a feedback as an input to a node, and effectively bind the actual edge that feeds it later in the graph.

> \[!IMPORTANT\]
> A graph containing one or more `csp.feedback` edges is still acyclic. The feedback connection will trigger a recomputation of the upstream node on the next engine cycle, which will be at the same engine time as the current cycle. Internally `csp.feedback` creates a pair of input and output adapters that are bound together.
- **`csp.feedback(ts_type)`**: `ts_type` is the type of the timeseries (ie int, str).
This returns an instance of a feedback object, which will *later* be bound to a downstream edge.
- **`out()`**: this method returns the timeseries edge which can be passed as an input to your node
- **`bind(ts)`**: this method is called to bind a downstream edge as the source of the feedback

Let us demonstrate the usage of `csp.feedback` using our wolves and elk example above. The graph code would look something like this:

```python
import csp
from csp import ts

@csp.node
def change_wolf_pop(elks: ts[int], wolf_only_factors: ts[float]) -> ts[int]:
# External factors could be anything here that affects the wolf population
# but not the elk population directly

# compute the desired wolf population here...

return 0

@csp.node
def compute_elk_pop(wolves: ts[int], elk_only_factors: ts[float]) -> ts[int]:
# Similarly external factors here only directly affect the elk population

# compute the new expected elk population here...

return 0

@csp.graph
def population_dynamics():
# create the feedback first so that we can refer to it later
elk_pop_fb = csp.feedback(int)

# update the wolf population, passing feedback out() which isn't bound yet
wolves = change_wolf_pop(elk_pop_fb.out(), csp.const(0.0))

# get elks output from compute_elk_pop
elks = compute_elk_pop(wolves, csp.const(0.0))

# now bind the elk population to the feedback, finishing the "loop"
elk_pop_fb.bind(elks)
```

We can visualize the graph using `csp.show_graph`. We see that it remains acyclic, but since the `FeedbackOutputDef` is bound to the `FeedbackInputDef` any output tick will loop back in at the next engine cycle.

![Output generated by show_graph](images/feedback-graph.png)

## `csp.DelayedEdge`

The delayed edge is similar to `csp.feedback` in the sense that it's a time series which is bound after its declared. Delayed edges must be bound *exactly* once and will raise an error during graph building if unbound.
Delayed edges can also not be used to create a cycle; if the edge is being bound to a downstream output, `csp.feedback` must be used instead. Any cycle will be detected by the CSP engine and raise a runtime error.

Delayed edges are useful when the exact input source needed is not known until graph-time; for example, you may want to subscribe to a list of data feeds which will only be known when you construct the graph.
They are also used by some advanced `csp.baselib` utilities like `DelayedCollect` and `DelayedDemultiplex` which help with input and output data processing.

An example usage of `csp.DelayedEdge` is below:

```python
import csp

@csp.graph
def delayed_edge():
delayed = csp.DelayedEdge(csp.ts[int])
three = csp.const(2) + delayed
delayed.bind(csp.const(1))
csp.print('three', three)
```

Executing this graph will give:

```
2020-01-01 00:00:00 three:3
```
52 changes: 0 additions & 52 deletions docs/wiki/how-tos/Add-Cycles-in-Graphs.md

This file was deleted.

Binary file added docs/wiki/images/feedback-graph.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 3333348

Please sign in to comment.