Skip to content

Commit 0683d35

Browse files
mesimoncopybara-github
authored andcommitted
Add past-clipping to sampler.
PiperOrigin-RevId: 962757853
1 parent c55e752 commit 0683d35

11 files changed

Lines changed: 875 additions & 172 deletions

File tree

dgf/src/api/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ py_library(
144144
"//dgf/src/sampling:beam_semi_distributed_sampler_v2",
145145
"//dgf/src/sampling:config",
146146
"//dgf/src/sampling:in_memory_sampler",
147+
"//dgf/src/sampling:temporal",
147148
"//dgf/src/sampling/gcp:spanner_graph_sampler",
148149
],
149150
)

dgf/src/learning/ten_lines/dataset_test.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ def test_per_sample_transformations(self):
276276
num_hops=1,
277277
hop_width=2,
278278
temporal_sampling=True,
279+
max_timeseries_len=5,
279280
),
280281
temporal=True,
281282
timeseries_pad_and_cap=pad_and_cap_config,
@@ -350,6 +351,7 @@ def test_default_no_per_sample_transforms(self):
350351
num_hops=1,
351352
hop_width=2,
352353
temporal_sampling=True,
354+
max_timeseries_len=3,
353355
),
354356
temporal=True,
355357
drop_remainder=False,
@@ -404,6 +406,7 @@ def test_sampler_returns_node_idxs_only_with_transforms_raises(self):
404406
num_hops=1,
405407
hop_width=2,
406408
temporal_sampling=True,
409+
max_timeseries_len=5,
407410
),
408411
temporal=True,
409412
timeseries_pad_and_cap=timeseries_transform.PadAndCapTimeseriesConfig(),
@@ -445,6 +448,7 @@ def test_timedelta_extraction_with_temporal_false(self):
445448
seed_nodeset="alerts",
446449
num_hops=1,
447450
hop_width=2,
451+
max_timeseries_len=5,
448452
),
449453
timeseries_pad_and_cap=timeseries_transform.PadAndCapTimeseriesConfig(
450454
sequence_length=5
@@ -482,6 +486,7 @@ def test_dynamic_set_sampler_returns_node_idxs_only_raises(self):
482486
num_hops=1,
483487
hop_width=2,
484488
temporal_sampling=True,
489+
max_timeseries_len=5,
485490
),
486491
temporal=True,
487492
timeseries_pad_and_cap=timeseries_transform.PadAndCapTimeseriesConfig(),
@@ -517,6 +522,7 @@ def test_timedelta_extraction_without_pad_and_cap_dynamic_ts_raises(self):
517522
num_hops=1,
518523
hop_width=2,
519524
temporal_sampling=True,
525+
max_timeseries_len=5,
520526
),
521527
temporal=True,
522528
timedelta_extraction=timeseries_transform.TimestampFeatureExtractorConfig(),

dgf/src/sampling/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ py_library(
3131
srcs = ["temporal.py"],
3232
deps = [
3333
"//dgf/src/data:in_memory_graph",
34+
"//dgf/src/data:schema",
3435
"//dgf/src/util:temporal",
3536
# numpy dep,
3637
],

dgf/src/sampling/config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ class SimpleSamplingConfig:
6161
grpah.
6262
temporal_sampling: If True, temporal sampling is enabled and causal
6363
timestamps are inferred from the schema.
64+
max_timeseries_len: The maximum number of historical causal sequence steps
65+
retained for each timeseries feature.
6466
"""
6567

6668
seed_nodeset: str
@@ -69,6 +71,7 @@ class SimpleSamplingConfig:
6971
reverse: bool = True
7072
with_replacement: bool = False
7173
temporal_sampling: bool = False
74+
max_timeseries_len: int = 32
7275

7376

7477
@dataclasses.dataclass
@@ -112,6 +115,8 @@ class SamplingPlan:
112115
timestamps are inferred from the schema.
113116
edgeset_timestamp_features: Mapping from edgeset name to its timestamp
114117
feature name for causal filtering.
118+
max_timeseries_len: The maximum number of historical causal sequence steps
119+
retained for each timeseries feature.
115120
"""
116121

117122
root: PlanNode
@@ -120,6 +125,7 @@ class SamplingPlan:
120125
edgeset_timestamp_features: Dict[str, str] = dataclasses.field(
121126
default_factory=dict
122127
)
128+
max_timeseries_len: int = 32
123129

124130

125131
def simple_sampling_config_to_sampling_plan(
@@ -177,5 +183,6 @@ def rec_build(nodeset: str, depth: int) -> PlanNode:
177183
with_replacement=src.with_replacement,
178184
temporal_sampling=src.temporal_sampling,
179185
edgeset_timestamp_features=edgeset_ts_features,
186+
max_timeseries_len=src.max_timeseries_len,
180187
)
181188

dgf/src/sampling/config_test.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,29 @@ def test_simple_sampling_config_to_sampling_config(self):
120120
)
121121
self.assertEqual(sampling_config, expected_sampling_config)
122122

123+
def test_simple_sampling_config_to_sampling_plan_with_max_timeseries_len(
124+
self,
125+
):
126+
schema = schema_lib.GraphSchema(
127+
node_sets={
128+
"n1": schema_lib.NodeSchema(features={}),
129+
},
130+
edge_sets={},
131+
)
132+
simple_config = config_lib.SimpleSamplingConfig(
133+
seed_nodeset="n1", num_hops=0, max_timeseries_len=10
134+
)
135+
plan = config_lib.simple_sampling_config_to_sampling_plan(
136+
simple_config, schema
137+
)
138+
self.assertEqual(plan.max_timeseries_len, 10)
139+
140+
def test_default_max_timeseries_len(self):
141+
simple_config = config_lib.SimpleSamplingConfig(seed_nodeset="n1")
142+
self.assertEqual(simple_config.max_timeseries_len, 32)
143+
plan = config_lib.SamplingPlan(root=config_lib.PlanNode(nodeset="n1"))
144+
self.assertEqual(plan.max_timeseries_len, 32)
145+
123146

124147
if __name__ == "__main__":
125148
absltest.main()

dgf/src/sampling/in_memory_sampler.py

Lines changed: 100 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -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

313321
def 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

349410
def 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

Comments
 (0)