Skip to content

Commit f33065d

Browse files
Clean up retail example and resolve conflicts with other docs PR (#356)
* Add retail example as a third Getting Started section Signed-off-by: Pavithra Eswaramoorthy <[email protected]> Signed-off-by: Adam Glustein <[email protected]> Co-authored-by: Pavithra Eswaramoorthy <[email protected]>
1 parent 455aa22 commit f33065d

File tree

5 files changed

+263
-2
lines changed

5 files changed

+263
-2
lines changed

docs/wiki/_Sidebar.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ Notes for editors:
1111
**Get Started (Tutorials)**
1212

1313
- [Installation](Installation)
14-
- [First steps](First-Steps)
14+
- [First Steps](First-Steps)
1515
- [More with CSP](More-with-CSP)
16+
- [Build a Basic App](More-with-CSP)
1617
- [IO with Adapters](IO-with-Adapters)
1718

1819
**Concepts**
@@ -28,7 +29,6 @@ Notes for editors:
2829
**How-to guides**
2930

3031
- [Use Statistical Nodes](Use-Statistical-Nodes)
31-
- Use Adapters (coming soon)
3232
- [Create Dynamic Baskets](Create-Dynamic-Baskets)
3333
- Write Adapters:
3434
- [Write Historical Input Adapters](Write-Historical-Input-Adapters)
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
We have looked at the features of CSP nodes and graphs, as well as how to run an application using `csp.run`. In this tutorial, we will apply what we learned in [First Steps](First-Steps) and [More with CSP](More-with-CSP) to build a basic retail app which maintains an online shopping cart.
2+
We will also introduce two important new concepts: the [`csp.Struct`](csp.Struct-API) data structure and multi-output nodes using `csp.Outputs`.
3+
4+
Our application will track a customer's shopping cart and apply a 10% discount for any items added to the cart in the first minute. Check out the complete code [here](examples/01_basics/e5_retail_cart.py).
5+
6+
## Structured data with `csp.Struct`
7+
8+
An individual item in a shopping cart consists of many fields; for example, the product's name, quantity and cost. The shopping cart itself may contain a list of these items as a field, plus a user ID or name. We also want to store updates to the shopping cart in an organized data structure, which has fields indicating the item in question and whether it was added or removed.
9+
10+
In `csp`, you can use a [`csp.Struct`](csp.Struct-API) to store typed fields together in a single data type. There are many advantages to using a `csp.Struct` instead of a standard Python dataclass. For example, the fields can be accessed as their own time series, ticking independently each time they update. Structs also have builtin conversion methods to JSON or dictionary objects. Due to their underlying C++ implementation, structs are also highly performant within `csp` compared to standard Python user-defined types.
11+
12+
```python
13+
import csp
14+
from typing import List
15+
16+
class Item(csp.Struct):
17+
name: str
18+
cost: float
19+
qty: int
20+
21+
class Cart(csp.Struct):
22+
user_id: int
23+
items: List[Item]
24+
25+
class CartUpdate(csp.Struct):
26+
item: Item
27+
add: bool
28+
```
29+
30+
Any number of fields on a struct can be set by a user; others will remain unset with a special value of `csp.UNSET`. For example, when we remove an item in `CartUpdate`, the cost will not be set.
31+
32+
## Track cart updates
33+
34+
Recall from [More with CSP](More-with-CSP) that we can store state variables in a `csp.node` using a `csp.state` block. We will create a node that tracks updates to a user's cart by storing the `Cart` struct as a state variable named `s_cart`.
35+
36+
> \[!TIP\]
37+
> By convention, state variables are prefixed with `s_` for readability.
38+
39+
A CSP node can return multiple named outputs. To annotate a multi-output node, we use `csp.Outputs` syntax for the return type annotation. To tick out each named value, we use the `csp.output` function. After each update event, we will tick out the total value of the user's cart and the number of items present.
40+
41+
To apply a discount for all items added in the first minute, we can use an alarm. We discussed how to use a `csp.alarm` as an internal time-series in the [Poisson counter example](More-with-CSP). We will only update the cart when the user adds, removes or purchases items. We need to know what the active discount rate to apply is but we don't need to trigger an update when it changes. To achieve this, we make the alarm time-series `discount` a *passive* input.
42+
43+
A *passive* input is a time-series input that will not cause the node to execute when it ticks. When we access the input within the node, we always get its most recent value. The opposite of passive inputs are *active* inputs, which trigger a node to compute upon a tick. So far, every input we've worked with has been an active input. We will set the discount input to be passive at graph startup.
44+
45+
> \[!TIP\]
46+
> By default, all `csp.ts` inputs are active. You can change the activity of an input at any point during execution by using `csp.make_passive` or `csp.make_active`.
47+
48+
```python
49+
from csp import ts
50+
from datetime import timedelta
51+
from functools import reduce
52+
53+
@csp.node
54+
def update_cart(event: ts[CartUpdate], user_id: int) -> csp.Outputs(total=ts[float], num_items=ts[int]):
55+
"""
56+
Track of the cart total and number of items.
57+
"""
58+
with csp.alarms():
59+
discount = csp.alarm(float)
60+
61+
with csp.state():
62+
# create an empty shopping cart
63+
s_cart = Cart(user_id=user_id, items=[])
64+
65+
with csp.start():
66+
csp.make_passive(discount)
67+
csp.schedule_alarm(discount, timedelta(), 0.9) # 10% off for the first minute
68+
csp.schedule_alarm(discount, timedelta(minutes=1), 1.0) # full price after!
69+
70+
if csp.ticked(event):
71+
if event.add:
72+
# apply current discount
73+
event.item.cost *= discount
74+
s_cart.items.append(event.item)
75+
else:
76+
# remove the given qty of the item
77+
new_items = []
78+
remaining_qty = event.item.qty
79+
for item in s_cart.items:
80+
if item.name == event.item.name:
81+
if item.qty > remaining_qty:
82+
item.qty -= remaining_qty
83+
new_items.append(item)
84+
else:
85+
remaining_qty -= item.qty
86+
else:
87+
new_items.append(item)
88+
s_cart.items = new_items
89+
90+
current_total = reduce(lambda a, b: a + b.cost * b.qty, s_cart.items, 0)
91+
current_num_items = reduce(lambda a, b: a + b.qty, s_cart.items, 0)
92+
csp.output(total=current_total, num_items=current_num_items)
93+
```
94+
95+
## Create workflow graph
96+
97+
To create example cart updates, we will use a [`csp.curve`](Base-Adapters-API#cspcurve) like we have in previous examples. The `csp.curve` replays a list of events at specific times.
98+
99+
```python
100+
st = datetime(2020, 1, 1)
101+
102+
@csp.graph
103+
def my_graph():
104+
# Example cart updates
105+
events = csp.curve(
106+
CartUpdate,
107+
[
108+
# Add 1 unit of X at $10 plus a 10% discount
109+
(st + timedelta(seconds=15), CartUpdate(item=Item(name="X", cost=10, qty=1), add=True)),
110+
# Add 2 units of Y at $15 each, plus a 10% discount
111+
(st + timedelta(seconds=30), CartUpdate(item=Item(name="Y", cost=15, qty=2), add=True)),
112+
# Remove 1 unit of Y
113+
(st + timedelta(seconds=45), CartUpdate(item=Item(name="Y", qty=1), add=False)),
114+
# Add 1 unit of Z at $20 but no discount, since our minute expired
115+
(st + timedelta(seconds=75), CartUpdate(item=Item(name="Z", cost=20, qty=1), add=True)),
116+
],
117+
)
118+
119+
csp.print("Events", events)
120+
121+
current_cart = update_cart(events, user_id=42)
122+
123+
csp.print("Cart number of items", current_cart.num_items)
124+
csp.print("Cart total", current_cart.total)
125+
```
126+
127+
## Execute the graph
128+
129+
Execute the program and observe the outputs that our shopping cart provides.
130+
131+
```python
132+
def main():
133+
csp.run(my_graph, starttime=st)
134+
```
135+
136+
```raw
137+
2020-01-01 00:00:15 Events:CartUpdate( item=Item( name=X, cost=10.0, qty=1 ), add=True )
138+
2020-01-01 00:00:15 Cart total:9.0
139+
2020-01-01 00:00:15 Cart number of items:1
140+
2020-01-01 00:00:30 Events:CartUpdate( item=Item( name=Y, cost=15.0, qty=2 ), add=True )
141+
2020-01-01 00:00:30 Cart total:36.0
142+
2020-01-01 00:00:30 Cart number of items:3
143+
2020-01-01 00:00:45 Events:CartUpdate( item=Item( name=Y, cost=<unset>, qty=1 ), add=False )
144+
2020-01-01 00:00:45 Cart total:22.5
145+
2020-01-01 00:00:45 Cart number of items:2
146+
2020-01-01 00:01:15 Events:CartUpdate( item=Item( name=Z, cost=20.0, qty=1 ), add=True )
147+
2020-01-01 00:01:15 Cart total:42.5
148+
2020-01-01 00:01:15 Cart number of items:3
149+
```

examples/01_basics/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
- [Ticking Graphs](./e2_ticking.py)
55
- [Visualizing a Graph](./e3_show_graph.py)
66
- [Complete Example (Trading)](./e4_trade_pnl.py)
7+
- [Complete Example (Retail)](./e5_retail_cart.py)
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""
2+
A retail cart example that:
3+
- Tracks the real-time total of a cart (as items are added and removed)
4+
- Applies a 10% discount if the purchase is made within 1 minute
5+
- Tracks the total sales and number of purchases
6+
"""
7+
8+
from datetime import datetime, timedelta
9+
from functools import reduce
10+
from typing import List
11+
12+
import csp
13+
from csp import ts
14+
15+
16+
class Item(csp.Struct):
17+
name: str
18+
cost: float
19+
qty: int
20+
21+
22+
class Cart(csp.Struct):
23+
user_id: int
24+
items: List[Item]
25+
26+
27+
class CartUpdate(csp.Struct):
28+
item: Item
29+
add: bool
30+
31+
32+
@csp.node
33+
def update_cart(event: ts[CartUpdate], user_id: int) -> csp.Outputs(total=ts[float], num_items=ts[int]):
34+
"""
35+
Track of the cart total and number of items.
36+
"""
37+
with csp.alarms():
38+
discount = csp.alarm(float)
39+
40+
with csp.state():
41+
# create an empty shopping cart
42+
s_cart = Cart(user_id=user_id, items=[])
43+
44+
with csp.start():
45+
csp.make_passive(discount)
46+
csp.schedule_alarm(discount, timedelta(), 0.9) # 10% off for the first minute
47+
csp.schedule_alarm(discount, timedelta(minutes=1), 1.0) # full price after!
48+
49+
if csp.ticked(event):
50+
if event.add:
51+
# apply current discount
52+
event.item.cost *= discount
53+
s_cart.items.append(event.item)
54+
else:
55+
# remove the given qty of the item
56+
new_items = []
57+
remaining_qty = event.item.qty
58+
for item in s_cart.items:
59+
if item.name == event.item.name:
60+
if item.qty > remaining_qty:
61+
item.qty -= remaining_qty
62+
new_items.append(item)
63+
else:
64+
remaining_qty -= item.qty
65+
else:
66+
new_items.append(item)
67+
s_cart.items = new_items
68+
69+
current_total = reduce(lambda a, b: a + b.cost * b.qty, s_cart.items, 0)
70+
current_num_items = reduce(lambda a, b: a + b.qty, s_cart.items, 0)
71+
csp.output(total=current_total, num_items=current_num_items)
72+
73+
74+
st = datetime(2020, 1, 1)
75+
76+
77+
@csp.graph
78+
def my_graph():
79+
# Example cart updates
80+
events = csp.curve(
81+
CartUpdate,
82+
[
83+
# Add 1 unit of X at $10 plus a 10% discount
84+
(st + timedelta(seconds=15), CartUpdate(item=Item(name="X", cost=10, qty=1), add=True)),
85+
# Add 2 units of Y at $15 each, plus a 10% discount
86+
(st + timedelta(seconds=30), CartUpdate(item=Item(name="Y", cost=15, qty=2), add=True)),
87+
# Remove 1 unit of Y
88+
(st + timedelta(seconds=45), CartUpdate(item=Item(name="Y", qty=1), add=False)),
89+
# Add 1 unit of Z at $20 but no discount, since our minute expired
90+
(st + timedelta(seconds=75), CartUpdate(item=Item(name="Z", cost=20, qty=1), add=True)),
91+
],
92+
)
93+
94+
csp.print("Events", events)
95+
96+
current_cart = update_cart(events, user_id=42)
97+
98+
csp.print("Cart number of items", current_cart.num_items)
99+
csp.print("Cart total", current_cart.total)
100+
101+
102+
def main():
103+
csp.run(my_graph, starttime=st)
104+
105+
106+
if __name__ == "__main__":
107+
main()

examples/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@
2727
<td><a href="./01_basics/e4_trade_pnl.py">Complete Example (Trading)</a></td>
2828
<td>Volume weighted average price (VWAP) and profit and loss (PnL)</td>
2929
</tr>
30+
<tr>
31+
<td><a href="./01_basics/e5_retail_cart.py">Complete Example (Retail)</a></td>
32+
<td>Maintain a shopping cart with time-based discounts for customers</td>
33+
</tr>
3034
<!-- Intermediate -->
3135
<tr>
3236
<td rowspan=4><a href="./02_intermediate/">Intermediate</a></td>

0 commit comments

Comments
 (0)