@@ -36,7 +36,7 @@ def __init__(
3636 return_node_idxs : bool ,
3737 schema : Optional [schema_lib .GraphSchema ] = None ,
3838 slice_timeseries_by_seed : bool = True ,
39- max_timeseries_len : Optional [ int ] = None ,
39+ max_timeseries_len : int = 32 ,
4040 has_temporal_edgesets : bool = False ,
4141 ):
4242 self ._cc_sampler = cc_sampler
@@ -49,10 +49,18 @@ def __init__(
4949 self ._has_temporal_edgesets = has_temporal_edgesets
5050 self ._timeseries_schema_cache = (
5151 temporal_util .extract_timeseries_schema_cache (self ._schema )
52- if self ._schema is not None and self . _slice_timeseries_by_seed
52+ if self ._schema is not None
5353 else None
5454 )
5555
56+ if self ._slice_timeseries_by_seed and self ._schema is None :
57+ raise ValueError (
58+ "schema must be provided when `slice_timeseries_by_seed=True`."
59+ )
60+
61+ if self ._max_timeseries_len <= 0 :
62+ raise ValueError ("max_timeseries_len must be positive" )
63+
5664 def set_return_options (self , return_features : bool , return_node_idxs : bool ):
5765 """Sets whether to return features and node indices in sampled graphs.
5866
@@ -117,6 +125,17 @@ def sample(
117125 """
118126
119127 # Check and convert the user input into what the c++ sampler expects.
128+ if (
129+ self ._return_features
130+ and self ._slice_timeseries_by_seed
131+ and self ._has_timeseries_features ()
132+ ):
133+ assert seed_timestamps is not None , (
134+ "`seed_timestamps` must be provided when"
135+ " `slice_timeseries_by_seed=True` and the schema contains"
136+ " `is_timeseries=True` features."
137+ )
138+
120139 return_single_graph = False
121140 if isinstance (seed_node_idxs , int ):
122141 return_single_graph = True
@@ -277,49 +296,51 @@ def _add_finalize_graphs(
277296
278297 If `_return_features` is True, full feature values are added.
279298 If `_return_node_idxs` is False, the "#idx" feature is removed.
280- If `seed_timestamps` and `_schema` are available and
281- `_slice_timeseries_by_seed` is True, any `is_timeseries=True` features are
282- causally filtered by the seed node timestamp.
299+ If `_return_features` is True and the schema has timeseries features:
300+ - If `_slice_timeseries_by_seed` is True, timeseries features are causally
301+ filtered by the seed node timestamp and clipped to `max_timeseries_len`.
302+ - Otherwise, timeseries features are clipped to `max_timeseries_len`.
283303
284304 Args:
285305 graphs: A list of `InMemoryGraph` objects to be finalized.
286306 seed_timestamps: Optional timestamps for causal timeseries filtering.
287307 """
288308 add_features_to_samples (
289- self ._full_graph , graphs , self ._return_features , self ._return_node_idxs
309+ full_graph = self ._full_graph ,
310+ samples = graphs ,
311+ return_features = self ._return_features ,
312+ return_node_idxs = self ._return_node_idxs ,
313+ schema = self ._schema ,
314+ seed_timestamps = seed_timestamps ,
315+ slice_timeseries_by_seed = self ._slice_timeseries_by_seed ,
316+ max_timeseries_len = self ._max_timeseries_len ,
317+ timeseries_schema_cache = self ._timeseries_schema_cache ,
290318 )
291- if self ._slice_timeseries_by_seed and self ._return_features :
292- if seed_timestamps is None and self ._has_timeseries_features ():
293- raise ValueError (
294- "`seed_timestamps` must be provided when"
295- " `slice_timeseries_by_seed=True` and the schema contains"
296- " `is_timeseries=True` features."
297- )
298- if seed_timestamps is not None :
299- if self ._schema is None :
300- raise ValueError (
301- "schema must be provided when `slice_timeseries_by_seed=True` and"
302- " `seed_timestamps` are passed."
303- )
304- for i , sample in enumerate (graphs ):
305- sampling_temporal_lib .filter_timeseries_by_timestamp (
306- graph = sample ,
307- schema_cache = self ._timeseries_schema_cache , # pyrefly: ignore[bad-argument-type]
308- target_timestamp = int (seed_timestamps [i ]),
309- max_timeseries_len = self ._max_timeseries_len ,
310- )
311319
312320
313321def add_features_to_samples (
314322 full_graph : in_memory_graph_lib .InMemoryGraph ,
315323 samples : List [in_memory_graph_lib .InMemoryGraph ],
316324 return_features : bool ,
317325 return_node_idxs : bool ,
326+ schema : Optional [schema_lib .GraphSchema ] = None ,
327+ seed_timestamps : Optional [np .ndarray ] = None ,
328+ slice_timeseries_by_seed : bool = False ,
329+ max_timeseries_len : int = 32 ,
330+ timeseries_schema_cache : Optional [
331+ temporal_util .TimeseriesSchemaCache
332+ ] = None ,
318333):
319334 """Adds features and optionally removes temporary node indices from sampled graphs.
320335
321336 If `return_features` is True, full feature values are copied from the
322- `full_graph` to the corresponding nodes in each graph within `graphs`.
337+ `full_graph` to the corresponding nodes in each graph within `samples`.
338+ If `schema` or `timeseries_schema_cache` is provided and contains timeseries
339+ features, extraction is fused with temporal filtering or clipping:
340+ - If `slice_timeseries_by_seed` is True, timeseries features are causally
341+ sliced up to each sample's `seed_timestamps` and capped to
342+ `max_timeseries_len`.
343+ - Otherwise, timeseries features are clipped to `max_timeseries_len`.
323344 If `return_node_idxs` is False, the "#idx" feature, which contains the
324345 original node indices, is removed from each node set in the sampled graphs.
325346
@@ -331,19 +352,59 @@ def add_features_to_samples(
331352 values from `full_graph`.
332353 return_node_idxs: Whether to keep the "#idx" feature in the sampled graphs.
333354 If False, this feature is removed.
355+ schema: Optional graph schema.
356+ seed_timestamps: Optional timestamps corresponding to each sample in
357+ `samples` for causal timeseries filtering.
358+ slice_timeseries_by_seed: Whether to causally slice timeseries features by
359+ the seed timestamp.
360+ max_timeseries_len: Optional cap on the maximum sequence length to retain.
361+ timeseries_schema_cache: Optional precomputed `TimeseriesSchemaCache`.
334362 """
335- if return_features or not return_node_idxs :
336- # Extract feature values.
337- # TODO(gbm): Do this in C++.
338- for sample in samples :
339- for node_set_name , node_set in sample .node_sets .items ():
340- node_idxs = node_set .features ["#idx" ]
341- if not return_node_idxs :
363+ if timeseries_schema_cache is None and schema is not None :
364+ timeseries_schema_cache = temporal_util .extract_timeseries_schema_cache (
365+ schema
366+ )
367+
368+ has_ts = (
369+ timeseries_schema_cache is not None
370+ and timeseries_schema_cache .has_timeseries
371+ )
372+
373+ for i , sample in enumerate (samples ):
374+ target_ts = None
375+ if has_ts and slice_timeseries_by_seed and seed_timestamps is not None :
376+ target_ts = int (seed_timestamps [i ])
377+
378+ if return_features :
379+ if has_ts and timeseries_schema_cache is not None :
380+ for node_set_name , node_set in sample .node_sets .items ():
381+ pk_name = timeseries_schema_cache .node_primary_keys .get (node_set_name )
382+ if (
383+ pk_name is not None
384+ and "#idx" in node_set .features
385+ and pk_name not in node_set .features
386+ ):
387+ node_set .features [pk_name ] = node_set .features ["#idx" ]
388+ sampling_temporal_lib .extract_features_timeseries (
389+ graph = sample ,
390+ source_graph = full_graph ,
391+ schema_cache = timeseries_schema_cache ,
392+ max_timeseries_len = max_timeseries_len ,
393+ target_timestamp = target_ts ,
394+ )
395+ else :
396+ for node_set_name , node_set in sample .node_sets .items ():
397+ if "#idx" in node_set .features :
398+ node_idxs = node_set .features ["#idx" ]
399+ for feature_name , feature_values in full_graph .node_sets [
400+ node_set_name
401+ ].features .items ():
402+ node_set .features [feature_name ] = feature_values [node_idxs ]
403+
404+ if not return_node_idxs :
405+ for node_set in sample .node_sets .values ():
406+ if "#idx" in node_set .features :
342407 del node_set .features ["#idx" ]
343- if return_features :
344- features = full_graph .node_sets [node_set_name ].features
345- for feature_name , full_feature_value in features .items ():
346- node_set .features [feature_name ] = full_feature_value [node_idxs ]
347408
348409
349410def create_sampler (
@@ -359,7 +420,6 @@ def create_sampler(
359420 seed : Optional [int ] = None ,
360421 edgeset_to_mask : Optional [str ] = None ,
361422 slice_timeseries_by_seed : Optional [bool ] = None ,
362- max_timeseries_len : Optional [int ] = None ,
363423) -> Sampler :
364424 """Creates an in-memory sampler.
365425
@@ -387,8 +447,6 @@ def create_sampler(
387447 slice_timeseries_by_seed: Whether to causally slice `is_timeseries=True`
388448 sequence features by the seed node timestamp. Defaults to
389449 `plan.temporal_sampling`.
390- max_timeseries_len: Optional cap on the number of historical causal sequence
391- steps retained for each timeseries feature.
392450
393451 TODO(gbm): Should we remove the compilation variations (e.g., change in random
394452 number generator, change in hashmaps).
@@ -440,6 +498,6 @@ def create_sampler(
440498 return_node_idxs = return_node_idxs ,
441499 schema = schema ,
442500 slice_timeseries_by_seed = slice_timeseries_by_seed ,
443- max_timeseries_len = max_timeseries_len ,
501+ max_timeseries_len = plan . max_timeseries_len ,
444502 has_temporal_edgesets = bool (edgeset_timestamp_features ),
445503 )
0 commit comments