-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #342 from Point72/document-delayed-edge-run-on-thread
Improve `csp.feedback` documentation with better example and document `DelayedEdge`
- Loading branch information
Showing
4 changed files
with
98 additions
and
53 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file was deleted.
Oops, something went wrong.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.