Skip to content

Commit 0f62a9f

Browse files
committed
Add optional argument to basketlib sync nodes in order to sync on an external trigger
Signed-off-by: Adam Glustein <Adam.Glustein@Point72.com>
1 parent 8335b43 commit 0f62a9f

5 files changed

Lines changed: 111 additions & 29 deletions

File tree

cpp/csp/cppnodes/basketlibimpl.cpp

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
namespace csp::cppnodes
55
{
66

7-
DECLARE_CPPNODE( _sync_list )
7+
DECLARE_CPPNODE( _sync_list_internal )
88
{
99
TS_LISTBASKET_INPUT( Generic, x );
10+
TS_INPUT( Generic, trigger );
11+
1012
SCALAR_INPUT( TimeDelta, threshold );
1113
SCALAR_INPUT( bool, output_incomplete );
14+
SCALAR_INPUT( bool, use_trigger );
1215

1316
ALARM( bool, a_end );
1417

@@ -18,7 +21,7 @@ DECLARE_CPPNODE( _sync_list )
1821

1922
TS_LISTBASKET_OUTPUT( Generic );
2023

21-
INIT_CPPNODE( _sync_list ) { }
24+
INIT_CPPNODE( _sync_list_internal ) { }
2225

2326
START()
2427
{
@@ -27,13 +30,14 @@ DECLARE_CPPNODE( _sync_list )
2730

2831
INVOKE()
2932
{
30-
if( x.tickedinputs() )
33+
if( s_alarm_handle.expired() )
3134
{
32-
if( s_alarm_handle.expired() )
33-
{
35+
if( !use_trigger || trigger.ticked() )
3436
s_alarm_handle = csp.schedule_alarm( a_end, threshold, true );
35-
}
37+
}
3638

39+
if( s_alarm_handle.active() && x.tickedinputs() )
40+
{
3741
for( auto it = x.tickedinputs(); it; ++it )
3842
{
3943
if( s_current_ticked[ it.elemId() ] == false )
@@ -70,7 +74,7 @@ DECLARE_CPPNODE( _sync_list )
7074
}
7175
};
7276

73-
EXPORT_CPPNODE( _sync_list );
77+
EXPORT_CPPNODE( _sync_list_internal );
7478

7579
/*
7680
@csp.node(cppimpl=_cspbasketlibimpl._sample_list)

cpp/csp/python/cspbasketlibimpl.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#include <csp/engine/CppNode.h>
22
#include <csp/python/PyCppNode.h>
33

4-
REGISTER_CPPNODE( csp::cppnodes, _sync_list );
4+
REGISTER_CPPNODE( csp::cppnodes, _sync_list_internal );
55
REGISTER_CPPNODE( csp::cppnodes, _sample_list );
66

77
static PyModuleDef _cspbasketlibimpl_module = {

csp/basketlib.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,22 @@
1111
Y = TypeVar("Y")
1212

1313

14-
@csp.node(cppimpl=_cspbasketlibimpl._sync_list)
15-
def sync_list(x: List[ts["T"]], threshold: timedelta, output_incomplete: bool = True) -> csp.OutputBasket(
16-
List[ts["T"]], shape_of="x"
17-
):
14+
@csp.node(cppimpl=_cspbasketlibimpl._sync_list_internal)
15+
def sync_list_internal(
16+
x: List[ts["T"]], trigger: ts["K"], threshold: timedelta, output_incomplete: bool, use_trigger: bool
17+
) -> csp.OutputBasket(List[ts["T"]], shape_of="x"):
1818
with csp.alarms():
1919
a_end = csp.alarm(bool)
2020

2121
with csp.state():
2222
s_current = {}
2323
s_alarm_handle = None
2424

25-
if csp.ticked(x):
26-
if not s_alarm_handle:
25+
if not s_alarm_handle:
26+
if not use_trigger or csp.ticked(trigger):
2727
s_alarm_handle = csp.schedule_alarm(a_end, threshold, True)
28+
29+
if s_alarm_handle and csp.ticked(x):
2830
s_current.update(x.tickeditems())
2931

3032
if csp.ticked(a_end) or len(s_current) == len(x):
@@ -37,19 +39,29 @@ def sync_list(x: List[ts["T"]], threshold: timedelta, output_incomplete: bool =
3739

3840

3941
@csp.graph
40-
def sync_dict(x: Dict["K", ts["T"]], threshold: timedelta, output_incomplete: bool = True) -> csp.OutputBasket(
41-
Dict["K", ts["T"]], shape_of="x"
42-
):
42+
def sync_list(
43+
x: List[ts["T"]], threshold: timedelta, output_incomplete: bool = True, trigger: ts["K"] = None
44+
) -> csp.OutputBasket(List[ts["T"]], shape_of="x"):
45+
use_trigger = trigger is not None
46+
if not use_trigger:
47+
trigger = csp.null_ts(bool)
48+
return sync_list_internal(x, trigger, threshold, output_incomplete, use_trigger)
49+
50+
51+
@csp.graph
52+
def sync_dict(
53+
x: Dict["K", ts["T"]], threshold: timedelta, output_incomplete: bool = True, trigger: ts["K"] = None
54+
) -> csp.OutputBasket(Dict["K", ts["T"]], shape_of="x"):
4355
values = list(x.values())
44-
synced = sync_list(values, threshold, output_incomplete)
56+
synced = sync_list(values, threshold, output_incomplete, trigger)
4557
return {k: v for k, v in zip(x.keys(), synced)}
4658

4759

48-
def sync(x, threshold: timedelta, output_incomplete: bool = True):
60+
def sync(x, threshold: timedelta, output_incomplete: bool = True, trigger: ts["K"] = None):
4961
if isinstance(x, list):
50-
return sync_list(x, threshold, output_incomplete)
62+
return sync_list(x, threshold, output_incomplete, trigger)
5163
elif isinstance(x, dict):
52-
return sync_dict(x, threshold, output_incomplete)
64+
return sync_dict(x, threshold, output_incomplete, trigger)
5365
raise ValueError(f"Input must be list or dict basket, got: {type(x)}")
5466

5567

csp/tests/test_basketlib.py

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,19 @@ def test_graph():
2828
random_floats_async = random_gen(trigger2)
2929

3030
# test _basket_synchronize_list
31-
synced_py = basketlib.sync_list.python(
32-
x=[random_floats1, random_floats_async], threshold=self.sync_threshold, output_incomplete=True
31+
synced_py = basketlib.sync_list_internal.python(
32+
x=[random_floats1, random_floats_async],
33+
trigger=csp.null_ts(bool),
34+
threshold=self.sync_threshold,
35+
output_incomplete=True,
36+
use_trigger=False,
3337
)
34-
synced_cpp = basketlib.sync_list(
35-
x=[random_floats1, random_floats_async], threshold=self.sync_threshold, output_incomplete=True
38+
synced_cpp = basketlib.sync_list_internal(
39+
x=[random_floats1, random_floats_async],
40+
trigger=csp.null_ts(bool),
41+
threshold=self.sync_threshold,
42+
output_incomplete=True,
43+
use_trigger=False,
3644
)
3745

3846
synced_auto_list = basketlib.sync(
@@ -132,6 +140,61 @@ def basic_graph():
132140
self.assertEqual(results["synced_complete_1"], [(datetime(2022, 6, 17, 9, 50), 6.0)])
133141
self.assertEqual(results["synced_complete_2"], [(datetime(2022, 6, 17, 9, 50), 9.0)])
134142

143+
def test_sync_basket_with_trigger(self):
144+
st = datetime(2020, 1, 1)
145+
146+
@csp.graph
147+
def graph():
148+
trigger = csp.curve(
149+
typ=str,
150+
data=[
151+
(st + timedelta(seconds=10), "trigger-1"), # regular case
152+
(st + timedelta(seconds=30), "trigger-2"), # exact same tick as a, includes it
153+
(st + timedelta(seconds=31), "trigger-ignored"), # in an active period, ignored
154+
(st + timedelta(seconds=50), "trigger-3"), # only a will get a tick here
155+
],
156+
)
157+
a = csp.curve(
158+
typ=float,
159+
data=[
160+
(st + timedelta(seconds=1), 1.0),
161+
(st + timedelta(seconds=12), 2.0),
162+
(st + timedelta(seconds=30), 3.0),
163+
(st + timedelta(seconds=52), 4.0),
164+
],
165+
)
166+
b = csp.curve(
167+
typ=float,
168+
data=[
169+
(st + timedelta(seconds=2), 5.0),
170+
(st + timedelta(seconds=14), 6.0),
171+
(st + timedelta(seconds=32), 7.0),
172+
(st + timedelta(seconds=56), 8.0),
173+
],
174+
)
175+
176+
synced_list = basketlib.sync_list(x=[a, b], threshold=timedelta(seconds=5), trigger=trigger)
177+
synced_dict = basketlib.sync_dict(x={"a": a, "b": b}, threshold=timedelta(seconds=5), trigger=trigger)
178+
179+
csp.add_graph_output("list_a", synced_list[0])
180+
csp.add_graph_output("list_b", synced_list[1])
181+
csp.add_graph_output("dict_a", synced_dict["a"])
182+
csp.add_graph_output("dict_b", synced_dict["b"])
183+
184+
result = csp.run(graph, starttime=st, endtime=st + timedelta(minutes=1))
185+
186+
expected_a = [
187+
(st + timedelta(seconds=14), 2.0),
188+
(st + timedelta(seconds=32), 3.0),
189+
(st + timedelta(seconds=55), 4.0),
190+
]
191+
expected_b = [(st + timedelta(seconds=14), 6.0), (st + timedelta(seconds=32), 7.0)]
192+
print(result["list_a"])
193+
self.assertEqual(result["list_a"], expected_a)
194+
self.assertEqual(result["list_b"], expected_b)
195+
self.assertEqual(result["dict_a"], expected_a)
196+
self.assertEqual(result["dict_b"], expected_b)
197+
135198
def test_sample_dict_basket(self):
136199
@csp.graph
137200
def graph():

docs/wiki/api-references/Basket-Nodes-API.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,27 @@ These functions are found in the `csp.basketlib` module and can be called using
1313
## `sync_list`
1414

1515
```python
16-
sync_list(x: List[ts["T"]], threshold: timedelta, output_incomplete: bool = True) → csp.OutputBasket(
16+
sync_list(x: List[ts["T"]], threshold: timedelta, output_incomplete: bool = True, trigger: ts["K"] = None) → csp.OutputBasket(
1717
List[ts["T"]], shape_of="x"
1818
)
1919
```
2020

2121
Synchronizes a list basket of time series within some threshold.
2222

23-
When any element of `x` first ticks, we wait up to `threshold` time for other elements to tick. Once all elements of the list basket tick at least once *or* the threshold elapses and `output_incomplete=True`, we return a list basket with the most recent value of each time series (between the interval's first tick and now) and reset the synchronization interval.
23+
If `trigger` is specified, then it will control when we begin the synchronization intervals. If it is not specified, any element of `x` will trigger the start of a synchronization interval.
24+
Once an interval is triggered, we wait up to `threshold` time for all elements of the basket `x` to tick. Once all elements of the list basket tick at least once *or* the threshold elapses and `output_incomplete=True`, we return a list basket with the most recent value of each time series (between the interval's first tick and now) and reset the synchronization interval.
2425

2526
Args:
2627

2728
- **`x`**: a list basket of time series to synchronize.
2829
- **`threshold`**: the time to wait for all elements of the basket to tick before propagating the values.
2930
- **`output_incomplete`**: if True, return an incomplete output basket if the threshold elapses before all values tick. Else, do not output in this situation.
31+
- **`trigger`**: an optional time-series which will trigger a synchronization period. If trigger is used, the first tick of `x` will not trigger a synchronization period.
3032

3133
## `sync_dict`
3234

3335
```python
34-
sync_dict(x: Dict["K", ts["T"]], threshold: timedelta, output_incomplete: bool = True) → csp.OutputBasket(
36+
sync_dict(x: Dict["K", ts["T"]], threshold: timedelta, output_incomplete: bool = True, trigger: ts["K"] = None) → csp.OutputBasket(
3537
Dict["K", ts["T"]], shape_of="x"
3638
)
3739
```
@@ -43,11 +45,12 @@ Args:
4345
- **`x`**: a dict basket of time series to synchronize.
4446
- **`threshold`**: the time to wait for all elements of the basket to tick before propagating the values.
4547
- **`output_incomplete`**: if True, return an incomplete output basket if the threshold elapses before all values tick. Else, do not output in this situation.
48+
- **`trigger`**: an optional time-series which will trigger a synchronization period. If trigger is used, the first tick of `x` will not trigger a synchronization period.
4649

4750
## `sync`
4851

4952
```python
50-
sync(x, threshold: timedelta, output_incomplete: bool = True)
53+
sync(x, threshold: timedelta, output_incomplete: bool = True, trigger: ts["K"] = None)
5154
```
5255

5356
Helper function which calls `sync_list` if x is a list basket and `sync_dict` if x is a dict basket. If x is not a valid basket, it will raise an exception.

0 commit comments

Comments
 (0)