Skip to content

Data

hypertorch.data

EnrichmentMode = Literal['concatenate', 'replace'] module-attribute

Mode used to combine generated features with existing features.

HyperedgeAttrsEnricher = HyperedgeEnricher module-attribute

Type alias for enrichers that generate hyperedge attributes.

HyperedgeWeightsEnricher = HyperedgeEnricher module-attribute

Type alias for enrichers that generate hyperedge weights.

NegativeSamplingSchedule = Literal['first_epoch', 'every_n_epochs', 'every_epoch'] module-attribute

Schedule controlling when negative samples are generated during training.

SamplingStrategy = SamplingStrategyEnum | SamplingStrategyLiteral module-attribute

Type for supported sampling strategies, either as an enum or a string literal.

SamplingStrategyLiteral = Literal['node', 'hyperedge'] module-attribute

Literal type for supported sampling strategies.

Dataset

Bases: Dataset

A dataset class for loading and processing hypergraph data.

Attributes:

Name Type Description
hdata HData

The hypergraph data stored by the dataset.

sampling_strategy SamplingStrategy

The strategy used for sampling sub-hypergraphs.

Source code in hypertorch/data/dataset.py
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
class Dataset(TorchDataset):
    """
    A dataset class for loading and processing hypergraph data.

    Attributes:
        hdata: The hypergraph data stored by the dataset.
        sampling_strategy: The strategy used for sampling sub-hypergraphs.
    """

    def __init__(
        self,
        hdata: HData | None = None,
        sampling_strategy: SamplingStrategy = SamplingStrategyEnum.HYPEREDGE,
        task: Task = TaskEnum.HYPERLINK_PREDICTION,
    ) -> None:
        """
        Initialize the dataset.

        Args:
            hdata: The processed hypergraph data in HData format.
            sampling_strategy: The strategy used for sampling sub-hypergraphs
                (e.g., by node IDs or hyperedge IDs).
                If not provided, defaults to ``SamplingStrategy.HYPEREDGE``.
            task: Learning task used when the HData in input is not provided.
                Defaults to ``"hyperlink-prediction"``.
        """
        self.__sampler: BaseSampler = create_sampler_from_strategy(sampling_strategy)
        self.sampling_strategy: SamplingStrategy = sampling_strategy
        self.task: Task = task
        self.hdata: HData = hdata if hdata is not None else HData.empty(task=task)

    def __len__(self) -> int:
        """
        Return the number of sampleable items in the dataset.

        Returns:
            length: Number of sampleable nodes or hyperedges, depending on the sampling strategy.
        """
        return self.__sampler.len(self.hdata)

    def __getitem__(self, index: int | list[int]) -> HData:
        """
        Sample a sub-hypergraph based on the sampling strategy and return it as HData.

        If:
            - Sampling by node IDs, the sub-hypergraph will contain all hyperedges incident to the
            sampled nodes and all nodes incident to those hyperedges.
            - Sampling by hyperedge IDs, the sub-hypergraph will contain all nodes incident to the
            sampled hyperedges.

        Args:
            index: An integer or a list of integers representing node or hyperedge IDs to sample,
                depending on the sampling strategy.

        Returns:
            hdata: An HData instance containing the sampled sub-hypergraph.

        Raises:
            ValueError: If the provided index is invalid (e.g., empty list or list length exceeds
                number of nodes/hyperedges).
            IndexError: If any node/hyperedge ID is out of bounds.
        """
        return self.__sampler.sample(index, self.hdata)

    @classmethod
    def from_hdata(
        cls,
        hdata: HData,
        sampling_strategy: SamplingStrategy = SamplingStrategyEnum.HYPEREDGE,
        task: Task = TaskEnum.HYPERLINK_PREDICTION,
    ) -> Dataset:
        """
        Create a `Dataset` instance from an `HData` object.

        Args:
            hdata: `HData` object containing the hypergraph data.
            sampling_strategy: The sampling strategy to use for the dataset. If not provided,
                defaults to ``SamplingStrategy.HYPEREDGE``.
            task: Learning task used when the HData. If not provided,
                defaults to ``"hyperlink-prediction"``.

        Returns:
            dataset: The `Dataset` instance with the provided `HData`.
        """
        return cls(hdata=hdata, sampling_strategy=sampling_strategy, task=task)

    @classmethod
    def from_url(
        cls,
        url: str,
        sampling_strategy: SamplingStrategyEnum = SamplingStrategyEnum.HYPEREDGE,
        task: Task = TaskEnum.HYPERLINK_PREDICTION,
        save_on_disk: bool = False,
    ) -> Dataset:
        """
        Create a `Dataset` instance by loading a hypergraph from a URL pointing to a .json or
        .json.zst file in HIF format.

        Args:
            url: The URL to the .json or .json.zst file containing the HIF hypergraph data.
                sampling_strategy: The sampling strategy to use for the dataset. If not provided,
                defaults to ``SamplingStrategy.HYPEREDGE``.
            task: Learning task used when the HData. If not provided,
                defaults to ``"hyperlink-prediction"``.
            save_on_disk: Whether to save the downloaded file on disk. Defaults to ``False``.

        Returns:
            dataset: The `Dataset` instance with the loaded hypergraph data.
        """
        hdata = HIFLoader.load_from_url(url=url, task=task, save_on_disk=save_on_disk)
        dataset = cls.from_hdata(hdata=hdata, sampling_strategy=sampling_strategy, task=task)
        return dataset

    @classmethod
    def from_path(
        cls,
        filepath: str,
        sampling_strategy: SamplingStrategyEnum = SamplingStrategyEnum.HYPEREDGE,
        task: TaskEnum = TaskEnum.HYPERLINK_PREDICTION,
    ) -> Dataset:
        """
        Create a `Dataset` instance by loading a hypergraph from a local file path pointing to a
        .json or .json.zst file in HIF format.

        Args:
            filepath: The local file path to the .json or .json.zst file containing the
                HIF hypergraph data.
            sampling_strategy: The sampling strategy to use for the dataset. If not provided,
                defaults to ``SamplingStrategy.HYPEREDGE``.
            task: Learning task used when the HData. If not provided,
                defaults to ``"hyperlink-prediction"``.

        Returns:
            dataset: The `Dataset` instance with the loaded hypergraph data.
        """
        hypergraph = HIFLoader.load_from_path(filepath=filepath, task=task)
        dataset = cls.from_hdata(hdata=hypergraph, sampling_strategy=sampling_strategy, task=task)
        return dataset

    def enrich_node_features(
        self,
        enricher: NodeEnricher,
        enrichment_mode: EnrichmentMode | None = None,
    ) -> None:
        """
        Enrich node features using the provided node feature enricher.

        Args:
            enricher: An instance of NodeEnricher to generate structural node features from
                hypergraph topology.
            enrichment_mode: How to combine generated features with existing ``hdata.x``.
                ``concatenate`` appends new features to the existing ones as additional columns.
                ``replace`` substitutes ``hdata.x`` entirely.
                Defaults to ``replace`` if not provided.
        """
        self.hdata = self.hdata.enrich_node_features(enricher, enrichment_mode)

    def enrich_node_features_from(
        self,
        dataset_with_features: Dataset,
        node_space_setting: NodeSpaceSetting = "transductive",
        fill_value: NodeSpaceFiller | None = None,
    ) -> None:
        """
        Enrich node features from another dataset by copying features by ``global_node_ids``.

        Examples:
            In a transductive setting, the full node space is preserved across datasets:
            >>> val_dataset.enrich_node_features_from(train_dataset)

            In inductive setting, missing node features can be filled with 0.0:
            >>> test_dataset.enrich_node_features_from(
            ...     train_dataset,
            ...     node_space_setting="inductive",
            ...     fill_value=0.0,  # torch.tensor(0.0) also works and will be broadcast to the
            ...     appropriate shape
            ... )

        Args:
            dataset_with_features: Source dataset providing node features.
            node_space_setting: The setting for the node space, determining how nodes are handled.
                ``transductive`` (default) preserves the full node space of the target dataset.
                ``inductive`` allows the target dataset to have a different node space, filling
                missing features with ``fill_value``.
            fill_value: Scalar or vector used to fill missing node features when
                ``node_space_setting`` is not transductive. Defaults to ``None``.

        Raises:
            ValueError: If the source dataset's node features cannot be aligned with the target
                dataset's nodes.
        """
        self.hdata = self.hdata.enrich_node_features_from(
            hdata_with_features=dataset_with_features.hdata,
            node_space_setting=node_space_setting,
            fill_value=fill_value,
        )

    def enrich_hyperedge_attr(
        self,
        enricher: HyperedgeEnricher,
        enrichment_mode: EnrichmentMode | None = None,
    ) -> None:
        """
        Enrich hyperedge attributes using the provided hyperedge feature enricher.

        Args:
            enricher: An instance of HyperedgeEnricher to generate structural hyperedge
                attributes from hypergraph topology.
            enrichment_mode: How to combine generated attributes with existing
                ``hdata.hyperedge_attr``.
                ``concatenate`` appends new attributes to the existing ones as additional columns.
                ``replace`` substitutes ``hdata.hyperedge_attr`` entirely.
                Defaults to ``replace`` if not provided.
        """
        self.hdata = self.hdata.enrich_hyperedge_attr(enricher, enrichment_mode)

    def enrich_hyperedge_weights(
        self,
        enricher: HyperedgeEnricher,
        enrichment_mode: EnrichmentMode | None = None,
    ) -> None:
        """
        Enrich hyperedge weights using the provided hyperedge weight enricher.

        Args:
            enricher: An instance of HyperedgeEnricher to generate structural hyperedge weights
                from hypergraph topology.
            enrichment_mode: How to combine generated weights with existing
                ``hdata.hyperedge_weights``.
                ``concatenate`` appends new weights to the existing ones as additional columns.
                ``replace`` substitutes ``hdata.hyperedge_weights`` entirely.
                Defaults to ``replace`` if not provided.
        """
        self.hdata = self.hdata.enrich_hyperedge_weights(enricher, enrichment_mode)

    def update_from_hdata(self, hdata: HData) -> Dataset:
        """
        Create a `Dataset` instance from an `HData` object.

        Args:
            hdata: `HData` object containing the hypergraph data.

        Returns:
            dataset: The `Dataset` instance with the provided `HData`.
        """
        return self.__class__(
            hdata=hdata,
            sampling_strategy=self.sampling_strategy,
            task=self.task,
        )

    def add_negative_samples(
        self,
        negative_sampler: NegativeSampler,
        seed: int | None = None,
    ) -> Dataset:
        """
        Create a new Dataset with sampled negative hyperedges added.

        Args:
            negative_sampler: Sampler used to generate negative hyperedges from
                this dataset's ``hdata``.
            seed: Optional random seed used for both negative sampling and the final shuffle.
                Defaults to ``None``.

        Returns:
            dataset: A new Dataset instance with positives and sampled negatives.
        """
        hdata_with_negatives = self.hdata.clone()
        hdata_with_negatives = hdata_with_negatives.add_negative_samples(
            negative_sampler=negative_sampler,
            seed=seed,
        )
        return self.update_from_hdata(hdata_with_negatives)

    def remove_hyperedges_with_fewer_than_k_nodes(
        self,
        k: int,
        preserve_global_node_ids: bool = False,
    ) -> None:
        """
        Remove hyperedges that have fewer than k incident nodes.

        Args:
            k: The minimum number of nodes a hyperedge must have to be retained.
            preserve_global_node_ids: Whether to preserve the global node IDs
                after removing hyperedges. Defaults to ``False``. If ``False``, the global node IDs
                will be reindexed to be contiguous after removing hyperedges.
                If ``True``, the global node IDs will be preserved, which may cause some models
                to raise as they may expect contiguous global node IDs.
        """
        self.hdata = self.hdata.remove_hyperedges_with_fewer_than_k_nodes(
            k, preserve_global_node_ids
        )

    def split(
        self,
        ratios: list[float] | None = None,
        shuffle: bool | None = False,
        seed: int | None = None,
        node_space_setting: NodeSpaceSetting = "transductive",
        sparse_split_hyperedges: bool = False,
        cover_all_nodes_in_train_split: bool = False,
        train_split_idx: int = 0,
        splitter: Splitter[Dataset, Any] | None = None,
    ) -> list[Dataset]:
        """
        Split the dataset based on task into partitions:
        - For node classification, splits are based on nodes and their incident hyperedges.
        - For hyperlink prediction, splits are based on hyperedges and their incident nodes.

        Boundaries are computed using cumulative floor to prevent early splits from
        over-consuming edges. The last split absorbs any rounding remainder.
        In the transductive setting, node-classification splits keep the full node space on
        the train split. Hyperlink-prediction splits keep the full hypergraph as context by
        default and mark supervised hyperedges with ``target_hyperedge_mask``.

        Splits that would end with zero hyperedges are rejected.

        Use ``split_with_ratios`` to also get the final ratios after splitting.

        Examples:
            Transductive split:
            >>> train, test = dataset.split([0.8, 0.2])
            >>> train.hdata.num_nodes == dataset.hdata.num_nodes
            >>> int(train.hdata.target_hyperedge_mask.sum().item()) == len(train)

            Inductive split:
            >>> train, test = dataset.split(
            ...     [0.8, 0.2],
            ...     node_space_setting="inductive",
            ... )
            >>> train.hdata.num_nodes <= dataset.hdata.num_nodes
            >>> test.hdata.num_nodes <= dataset.hdata.num_nodes

        Args:
            ratios: List of floats summing to ``1.0``, e.g., ``[0.8, 0.1, 0.1]``.
            shuffle: Whether to shuffle hyperedges before splitting.
                Defaults to ``False`` for deterministic splits.
            seed: Optional random seed for reproducibility. Ignored if shuffle is set to ``False``.
            node_space_setting: Whether to preserve the full node space in the splits.
                ``transductive`` (default) preserves the full node space on the
                first split. ``inductive`` keeps each split's local node space.
            sparse_split_hyperedges: Whether hyperlink-prediction splits should use the
                sparse split behavior. Defaults to ``False``, which keeps the full
                hypergraph as context in transductive splits and marks supervised hyperedges with
                ``target_hyperedge_mask``.
            cover_all_nodes_in_train_split: Whether a transductive sparse hyperedge split
                should move hyperedges from later splits until every node is
                incident to one of its selected hyperedges. Ratios are approximate
                when this coverage requires moving hyperedges into the first split.
            train_split_idx: The index of the split to treat as the train split. Defaults to ``0``,
                so the first split is the train split that is optionally rebalanced to cover
                all nodes in sparse transductive hyperlink-prediction splits.
                This is used only when ``node_space_setting=="transductive"``
                and ``cover_all_nodes_in_train_split==True``,
                to determine which split should be rebalanced to cover all nodes.
                For the 'inductive' setting, splits are always returned based on
                the provided ratios.
            splitter: Optional dataset splitter. When provided, it owns split
                construction and final-ratio reporting. Defaults to ``None``.

        Returns:
            datasets: List of Dataset objects, one per split, each with contiguous IDs.

        Raises:
            ValueError: If ratios do not sum to ``1.0``, a final split has zero
                hyperedges, if train coverage is requested for dense hyperlink-prediction
                splits, or a requested sparse train-cover split cannot cover the full node
                space.
        """
        if splitter is not None:
            return splitter.split(self)

        if ratios is None:
            raise ValueError("'ratios' must be provided when no custom 'splitter' is provided.")

        if self.hdata.is_node_related_task:
            splits, _ = NodeDatasetSplitter(
                node_space_setting=node_space_setting,
                shuffle=shuffle,
                seed=seed,
            ).split(
                to_split=self,
                ratios=ratios,
            )
            return splits

        if not self.hdata.is_hyperedge_related_task:
            raise ValueError(f"Unsupported task category for task={self.hdata.task!r}.")

        hyperedge_splitter_cls = (
            SparseHyperedgeDatasetSplitter if sparse_split_hyperedges else HyperedgeDatasetSplitter
        )
        splits, _ = hyperedge_splitter_cls(
            node_space_setting=node_space_setting,
            shuffle=shuffle,
            seed=seed,
        ).split(
            to_split=self,
            ratios=ratios,
            train_split_idx=train_split_idx,
            cover_all_nodes_in_train_split=cover_all_nodes_in_train_split,
        )
        return splits

    def split_with_ratios(
        self,
        ratios: list[float],
        shuffle: bool | None = False,
        seed: int | None = None,
        node_space_setting: NodeSpaceSetting = "transductive",
        cover_all_nodes_in_train_split: bool = False,
        train_split_idx: int = 0,
        sparse_split_hyperedges: bool = False,
    ) -> tuple[list[Dataset], list[float]]:
        """
        Split the dataset based on task and return the final hyperedge ratios:
        - For node classification, splits are based on nodes and their incident hyperedges.
          For more details, look at the `NodeDatasetSplitter` class.
        - For hyperlink prediction, splits are based on hyperedges and their incident nodes.
          For more details, look at the `HyperedgeDatasetSplitter` class.

        Boundaries are computed using cumulative floor to prevent early splits from
        over-consuming edges. The last split absorbs any rounding remainder.
        In the transductive setting, node-classification splits keep the full node space on
        the train split. Hyperlink-prediction splits keep the full hypergraph as context by
        default and mark supervised hyperedges with ``target_hyperedge_mask``.

        Splits that would end with zero hyperedges are rejected.

        Final ratios are computed from split hyperedge counts after ratio
        boundaries and any requested transductive rebalancing have been applied.

        To provide a custom splitting implementation, use the ``splitter``
        argument of the ``split`` method instead.

        Args:
            ratios: List of floats summing to ``1.0``, e.g., ``[0.8, 0.1, 0.1]``.
            shuffle: Whether to shuffle hyperedges before splitting. Defaults to
                ``False`` for deterministic splits.
            node_space_setting: Whether to preserve the full node space in the
                splits. ``transductive`` (default) preserves the full node space
                on the first split. ``inductive`` keeps each split's local node space.
            sparse_split_hyperedges: Whether hyperlink-prediction splits should use the legacy
                sparse materialization behavior. Defaults to ``False``, which keeps the full
                hypergraph as context in transductive splits and marks supervised hyperedges
                with ``target_hyperedge_mask``.
            cover_all_nodes_in_train_split: Whether a transductive sparse hyperedge split
                should move hyperedges from later splits until every node is
                incident to one of its selected hyperedges.
            train_split_idx: The index of the split to treat as the train split. Defaults to ``0``,
                so the first split is the train split that is optionally rebalanced to cover
                all nodes in sparse transductive hyperlink-prediction splits.
                This is used only when ``node_space_setting=="transductive"``
                and ``cover_all_nodes_in_train_split==True``,
                to determine which split should be rebalanced to cover all nodes.
                For the 'inductive' setting, splits are always returned based on
                the provided ratios.
            seed: Optional random seed for reproducibility. Ignored if ``shuffle``
                is set to ``False``. Defaults to ``None``.

        Returns:
            datasets: List of Dataset instances, one per split, each with contiguous IDs.
            final_ratios: List of floats representing the actual ratios of target hyperedges
                in each split after splitting and any requested rebalancing.

        Raises:
            ValueError: If ratios do not sum to ``1.0``, a final split has zero
                hyperedges, or a requested transductive train-cover split cannot
                cover the full node space.
        """
        if self.hdata.is_node_related_task:
            return NodeDatasetSplitter(
                node_space_setting=node_space_setting,
                shuffle=shuffle,
                seed=seed,
            ).split(
                to_split=self,
                ratios=ratios,
            )

        if not self.hdata.is_hyperedge_related_task:
            raise ValueError(f"Unsupported task category for task={self.hdata.task!r}.")

        hyperedge_splitter_cls = (
            SparseHyperedgeDatasetSplitter if sparse_split_hyperedges else HyperedgeDatasetSplitter
        )
        return hyperedge_splitter_cls(
            node_space_setting=node_space_setting,
            shuffle=shuffle,
            seed=seed,
        ).split(
            to_split=self,
            ratios=ratios,
            train_split_idx=train_split_idx,
            cover_all_nodes_in_train_split=cover_all_nodes_in_train_split,
        )

    def to(self, device: torch.device) -> Dataset:
        """
        Move the dataset's HData to the specified device.

        Args:
            device: The target device (e.g., ``torch.device('cuda')`` or ``torch.device('cpu')``).

        Returns:
            dataset: The Dataset instance moved to the specified device.
        """
        self.hdata = self.hdata.to(device)
        return self

    def transform_node_attrs(
        self,
        attrs: dict[str, Any],
        attr_keys: list[str] | None = None,
    ) -> Tensor:
        """
        Transform HIF node attributes into a numeric tensor.
        Overload this in case processing node attributes requires custom logic.

        Args:
            attrs: Attributes to transform.
            attr_keys: Optional attribute key order used for consistent output shape.
                Defaults to ``None``.

        Returns:
            attrs: Tensor containing numeric attribute values.
        """
        return HIFProcessor.transform_attrs(attrs, attr_keys)

    def transform_hyperedge_attrs(
        self,
        attrs: dict[str, Any],
        attr_keys: list[str] | None = None,
    ) -> Tensor:
        """
        Transform hyperedge attributes into a numeric tensor.
        Overload this in case processing hyperedge attributes requires custom logic.

        Args:
            attrs: Attributes to transform.
            attr_keys: Optional attribute key order used for consistent output shape.
                Defaults to ``None``.

        Returns:
            attrs: Tensor containing numeric attribute values.
        """
        return HIFProcessor.transform_attrs(attrs, attr_keys)

    def stats(self) -> dict[str, Any]:
        """
        Compute statistics for the dataset.

        This method currently delegates to the underlying HData's stats method.

        Fields:
            - ``shape_x``: The shape of the node feature matrix ``x``.
            - ``shape_hyperedge_attr``: The shape of the hyperedge attribute matrix, or ``None``
            if hyperedge attributes are not present.
            - ``num_nodes``: The number of nodes in the hypergraph.
            - ``num_hyperedges``: The number of hyperedges in the hypergraph.
            - ``avg_degree_node_raw``: The average degree of nodes, calculated as the mean number
             of hyperedges each node belongs to.
            - ``avg_degree_node``: The floored node average degree.
            - ``avg_degree_hyperedge_raw``: The average size of hyperedges, calculated as the
            mean number of nodes each hyperedge contains.
            - ``avg_degree_hyperedge``: The floored hyperedge average size.
            - ``node_degree_max``: The maximum degree of any node in the hypergraph.
            - ``hyperedge_degree_max``: The maximum size of any hyperedge in the hypergraph.
            - ``node_degree_median``: The median degree of nodes in the hypergraph.
            - ``hyperedge_degree_median``: The median size of hyperedges in the hypergraph.
            - ``distribution_node_degree``: A list where the value at index ``i`` represents
            the count of nodes with degree ``i``.
            - ``distribution_hyperedge_size``: A list where the value at index ``i`` represents
            the count of hyperedges with size ``i``.
            - ``distribution_node_degree_hist``: A dictionary where the keys are node degrees
             and the values are the count of nodes with that degree.
            - ``distribution_hyperedge_size_hist``: A dictionary where the keys are hyperedge
             sizes and the values are the count of hyperedges with that size.

        Returns:
            stats: A dictionary containing various statistics about the hypergraph.
        """
        return self.hdata.stats()

__init__(hdata=None, sampling_strategy=SamplingStrategyEnum.HYPEREDGE, task=TaskEnum.HYPERLINK_PREDICTION)

Initialize the dataset.

Parameters:

Name Type Description Default
hdata HData | None

The processed hypergraph data in HData format.

None
sampling_strategy SamplingStrategy

The strategy used for sampling sub-hypergraphs (e.g., by node IDs or hyperedge IDs). If not provided, defaults to SamplingStrategy.HYPEREDGE.

HYPEREDGE
task Task

Learning task used when the HData in input is not provided. Defaults to "hyperlink-prediction".

HYPERLINK_PREDICTION
Source code in hypertorch/data/dataset.py
def __init__(
    self,
    hdata: HData | None = None,
    sampling_strategy: SamplingStrategy = SamplingStrategyEnum.HYPEREDGE,
    task: Task = TaskEnum.HYPERLINK_PREDICTION,
) -> None:
    """
    Initialize the dataset.

    Args:
        hdata: The processed hypergraph data in HData format.
        sampling_strategy: The strategy used for sampling sub-hypergraphs
            (e.g., by node IDs or hyperedge IDs).
            If not provided, defaults to ``SamplingStrategy.HYPEREDGE``.
        task: Learning task used when the HData in input is not provided.
            Defaults to ``"hyperlink-prediction"``.
    """
    self.__sampler: BaseSampler = create_sampler_from_strategy(sampling_strategy)
    self.sampling_strategy: SamplingStrategy = sampling_strategy
    self.task: Task = task
    self.hdata: HData = hdata if hdata is not None else HData.empty(task=task)

__len__()

Return the number of sampleable items in the dataset.

Returns:

Name Type Description
length int

Number of sampleable nodes or hyperedges, depending on the sampling strategy.

Source code in hypertorch/data/dataset.py
def __len__(self) -> int:
    """
    Return the number of sampleable items in the dataset.

    Returns:
        length: Number of sampleable nodes or hyperedges, depending on the sampling strategy.
    """
    return self.__sampler.len(self.hdata)

__getitem__(index)

Sample a sub-hypergraph based on the sampling strategy and return it as HData.

If
  • Sampling by node IDs, the sub-hypergraph will contain all hyperedges incident to the sampled nodes and all nodes incident to those hyperedges.
  • Sampling by hyperedge IDs, the sub-hypergraph will contain all nodes incident to the sampled hyperedges.

Parameters:

Name Type Description Default
index int | list[int]

An integer or a list of integers representing node or hyperedge IDs to sample, depending on the sampling strategy.

required

Returns:

Name Type Description
hdata HData

An HData instance containing the sampled sub-hypergraph.

Raises:

Type Description
ValueError

If the provided index is invalid (e.g., empty list or list length exceeds number of nodes/hyperedges).

IndexError

If any node/hyperedge ID is out of bounds.

Source code in hypertorch/data/dataset.py
def __getitem__(self, index: int | list[int]) -> HData:
    """
    Sample a sub-hypergraph based on the sampling strategy and return it as HData.

    If:
        - Sampling by node IDs, the sub-hypergraph will contain all hyperedges incident to the
        sampled nodes and all nodes incident to those hyperedges.
        - Sampling by hyperedge IDs, the sub-hypergraph will contain all nodes incident to the
        sampled hyperedges.

    Args:
        index: An integer or a list of integers representing node or hyperedge IDs to sample,
            depending on the sampling strategy.

    Returns:
        hdata: An HData instance containing the sampled sub-hypergraph.

    Raises:
        ValueError: If the provided index is invalid (e.g., empty list or list length exceeds
            number of nodes/hyperedges).
        IndexError: If any node/hyperedge ID is out of bounds.
    """
    return self.__sampler.sample(index, self.hdata)

from_hdata(hdata, sampling_strategy=SamplingStrategyEnum.HYPEREDGE, task=TaskEnum.HYPERLINK_PREDICTION) classmethod

Create a Dataset instance from an HData object.

Parameters:

Name Type Description Default
hdata HData

HData object containing the hypergraph data.

required
sampling_strategy SamplingStrategy

The sampling strategy to use for the dataset. If not provided, defaults to SamplingStrategy.HYPEREDGE.

HYPEREDGE
task Task

Learning task used when the HData. If not provided, defaults to "hyperlink-prediction".

HYPERLINK_PREDICTION

Returns:

Name Type Description
dataset Dataset

The Dataset instance with the provided HData.

Source code in hypertorch/data/dataset.py
@classmethod
def from_hdata(
    cls,
    hdata: HData,
    sampling_strategy: SamplingStrategy = SamplingStrategyEnum.HYPEREDGE,
    task: Task = TaskEnum.HYPERLINK_PREDICTION,
) -> Dataset:
    """
    Create a `Dataset` instance from an `HData` object.

    Args:
        hdata: `HData` object containing the hypergraph data.
        sampling_strategy: The sampling strategy to use for the dataset. If not provided,
            defaults to ``SamplingStrategy.HYPEREDGE``.
        task: Learning task used when the HData. If not provided,
            defaults to ``"hyperlink-prediction"``.

    Returns:
        dataset: The `Dataset` instance with the provided `HData`.
    """
    return cls(hdata=hdata, sampling_strategy=sampling_strategy, task=task)

from_url(url, sampling_strategy=SamplingStrategyEnum.HYPEREDGE, task=TaskEnum.HYPERLINK_PREDICTION, save_on_disk=False) classmethod

Create a Dataset instance by loading a hypergraph from a URL pointing to a .json or .json.zst file in HIF format.

Parameters:

Name Type Description Default
url str

The URL to the .json or .json.zst file containing the HIF hypergraph data. sampling_strategy: The sampling strategy to use for the dataset. If not provided, defaults to SamplingStrategy.HYPEREDGE.

required
task Task

Learning task used when the HData. If not provided, defaults to "hyperlink-prediction".

HYPERLINK_PREDICTION
save_on_disk bool

Whether to save the downloaded file on disk. Defaults to False.

False

Returns:

Name Type Description
dataset Dataset

The Dataset instance with the loaded hypergraph data.

Source code in hypertorch/data/dataset.py
@classmethod
def from_url(
    cls,
    url: str,
    sampling_strategy: SamplingStrategyEnum = SamplingStrategyEnum.HYPEREDGE,
    task: Task = TaskEnum.HYPERLINK_PREDICTION,
    save_on_disk: bool = False,
) -> Dataset:
    """
    Create a `Dataset` instance by loading a hypergraph from a URL pointing to a .json or
    .json.zst file in HIF format.

    Args:
        url: The URL to the .json or .json.zst file containing the HIF hypergraph data.
            sampling_strategy: The sampling strategy to use for the dataset. If not provided,
            defaults to ``SamplingStrategy.HYPEREDGE``.
        task: Learning task used when the HData. If not provided,
            defaults to ``"hyperlink-prediction"``.
        save_on_disk: Whether to save the downloaded file on disk. Defaults to ``False``.

    Returns:
        dataset: The `Dataset` instance with the loaded hypergraph data.
    """
    hdata = HIFLoader.load_from_url(url=url, task=task, save_on_disk=save_on_disk)
    dataset = cls.from_hdata(hdata=hdata, sampling_strategy=sampling_strategy, task=task)
    return dataset

from_path(filepath, sampling_strategy=SamplingStrategyEnum.HYPEREDGE, task=TaskEnum.HYPERLINK_PREDICTION) classmethod

Create a Dataset instance by loading a hypergraph from a local file path pointing to a .json or .json.zst file in HIF format.

Parameters:

Name Type Description Default
filepath str

The local file path to the .json or .json.zst file containing the HIF hypergraph data.

required
sampling_strategy SamplingStrategyEnum

The sampling strategy to use for the dataset. If not provided, defaults to SamplingStrategy.HYPEREDGE.

HYPEREDGE
task TaskEnum

Learning task used when the HData. If not provided, defaults to "hyperlink-prediction".

HYPERLINK_PREDICTION

Returns:

Name Type Description
dataset Dataset

The Dataset instance with the loaded hypergraph data.

Source code in hypertorch/data/dataset.py
@classmethod
def from_path(
    cls,
    filepath: str,
    sampling_strategy: SamplingStrategyEnum = SamplingStrategyEnum.HYPEREDGE,
    task: TaskEnum = TaskEnum.HYPERLINK_PREDICTION,
) -> Dataset:
    """
    Create a `Dataset` instance by loading a hypergraph from a local file path pointing to a
    .json or .json.zst file in HIF format.

    Args:
        filepath: The local file path to the .json or .json.zst file containing the
            HIF hypergraph data.
        sampling_strategy: The sampling strategy to use for the dataset. If not provided,
            defaults to ``SamplingStrategy.HYPEREDGE``.
        task: Learning task used when the HData. If not provided,
            defaults to ``"hyperlink-prediction"``.

    Returns:
        dataset: The `Dataset` instance with the loaded hypergraph data.
    """
    hypergraph = HIFLoader.load_from_path(filepath=filepath, task=task)
    dataset = cls.from_hdata(hdata=hypergraph, sampling_strategy=sampling_strategy, task=task)
    return dataset

enrich_node_features(enricher, enrichment_mode=None)

Enrich node features using the provided node feature enricher.

Parameters:

Name Type Description Default
enricher NodeEnricher

An instance of NodeEnricher to generate structural node features from hypergraph topology.

required
enrichment_mode EnrichmentMode | None

How to combine generated features with existing hdata.x. concatenate appends new features to the existing ones as additional columns. replace substitutes hdata.x entirely. Defaults to replace if not provided.

None
Source code in hypertorch/data/dataset.py
def enrich_node_features(
    self,
    enricher: NodeEnricher,
    enrichment_mode: EnrichmentMode | None = None,
) -> None:
    """
    Enrich node features using the provided node feature enricher.

    Args:
        enricher: An instance of NodeEnricher to generate structural node features from
            hypergraph topology.
        enrichment_mode: How to combine generated features with existing ``hdata.x``.
            ``concatenate`` appends new features to the existing ones as additional columns.
            ``replace`` substitutes ``hdata.x`` entirely.
            Defaults to ``replace`` if not provided.
    """
    self.hdata = self.hdata.enrich_node_features(enricher, enrichment_mode)

enrich_node_features_from(dataset_with_features, node_space_setting='transductive', fill_value=None)

Enrich node features from another dataset by copying features by global_node_ids.

Examples:

In a transductive setting, the full node space is preserved across datasets:

>>> val_dataset.enrich_node_features_from(train_dataset)

In inductive setting, missing node features can be filled with 0.0:

>>> test_dataset.enrich_node_features_from(
...     train_dataset,
...     node_space_setting="inductive",
...     fill_value=0.0,  # torch.tensor(0.0) also works and will be broadcast to the
...     appropriate shape
... )

Parameters:

Name Type Description Default
dataset_with_features Dataset

Source dataset providing node features.

required
node_space_setting NodeSpaceSetting

The setting for the node space, determining how nodes are handled. transductive (default) preserves the full node space of the target dataset. inductive allows the target dataset to have a different node space, filling missing features with fill_value.

'transductive'
fill_value NodeSpaceFiller | None

Scalar or vector used to fill missing node features when node_space_setting is not transductive. Defaults to None.

None

Raises:

Type Description
ValueError

If the source dataset's node features cannot be aligned with the target dataset's nodes.

Source code in hypertorch/data/dataset.py
def enrich_node_features_from(
    self,
    dataset_with_features: Dataset,
    node_space_setting: NodeSpaceSetting = "transductive",
    fill_value: NodeSpaceFiller | None = None,
) -> None:
    """
    Enrich node features from another dataset by copying features by ``global_node_ids``.

    Examples:
        In a transductive setting, the full node space is preserved across datasets:
        >>> val_dataset.enrich_node_features_from(train_dataset)

        In inductive setting, missing node features can be filled with 0.0:
        >>> test_dataset.enrich_node_features_from(
        ...     train_dataset,
        ...     node_space_setting="inductive",
        ...     fill_value=0.0,  # torch.tensor(0.0) also works and will be broadcast to the
        ...     appropriate shape
        ... )

    Args:
        dataset_with_features: Source dataset providing node features.
        node_space_setting: The setting for the node space, determining how nodes are handled.
            ``transductive`` (default) preserves the full node space of the target dataset.
            ``inductive`` allows the target dataset to have a different node space, filling
            missing features with ``fill_value``.
        fill_value: Scalar or vector used to fill missing node features when
            ``node_space_setting`` is not transductive. Defaults to ``None``.

    Raises:
        ValueError: If the source dataset's node features cannot be aligned with the target
            dataset's nodes.
    """
    self.hdata = self.hdata.enrich_node_features_from(
        hdata_with_features=dataset_with_features.hdata,
        node_space_setting=node_space_setting,
        fill_value=fill_value,
    )

enrich_hyperedge_attr(enricher, enrichment_mode=None)

Enrich hyperedge attributes using the provided hyperedge feature enricher.

Parameters:

Name Type Description Default
enricher HyperedgeEnricher

An instance of HyperedgeEnricher to generate structural hyperedge attributes from hypergraph topology.

required
enrichment_mode EnrichmentMode | None

How to combine generated attributes with existing hdata.hyperedge_attr. concatenate appends new attributes to the existing ones as additional columns. replace substitutes hdata.hyperedge_attr entirely. Defaults to replace if not provided.

None
Source code in hypertorch/data/dataset.py
def enrich_hyperedge_attr(
    self,
    enricher: HyperedgeEnricher,
    enrichment_mode: EnrichmentMode | None = None,
) -> None:
    """
    Enrich hyperedge attributes using the provided hyperedge feature enricher.

    Args:
        enricher: An instance of HyperedgeEnricher to generate structural hyperedge
            attributes from hypergraph topology.
        enrichment_mode: How to combine generated attributes with existing
            ``hdata.hyperedge_attr``.
            ``concatenate`` appends new attributes to the existing ones as additional columns.
            ``replace`` substitutes ``hdata.hyperedge_attr`` entirely.
            Defaults to ``replace`` if not provided.
    """
    self.hdata = self.hdata.enrich_hyperedge_attr(enricher, enrichment_mode)

enrich_hyperedge_weights(enricher, enrichment_mode=None)

Enrich hyperedge weights using the provided hyperedge weight enricher.

Parameters:

Name Type Description Default
enricher HyperedgeEnricher

An instance of HyperedgeEnricher to generate structural hyperedge weights from hypergraph topology.

required
enrichment_mode EnrichmentMode | None

How to combine generated weights with existing hdata.hyperedge_weights. concatenate appends new weights to the existing ones as additional columns. replace substitutes hdata.hyperedge_weights entirely. Defaults to replace if not provided.

None
Source code in hypertorch/data/dataset.py
def enrich_hyperedge_weights(
    self,
    enricher: HyperedgeEnricher,
    enrichment_mode: EnrichmentMode | None = None,
) -> None:
    """
    Enrich hyperedge weights using the provided hyperedge weight enricher.

    Args:
        enricher: An instance of HyperedgeEnricher to generate structural hyperedge weights
            from hypergraph topology.
        enrichment_mode: How to combine generated weights with existing
            ``hdata.hyperedge_weights``.
            ``concatenate`` appends new weights to the existing ones as additional columns.
            ``replace`` substitutes ``hdata.hyperedge_weights`` entirely.
            Defaults to ``replace`` if not provided.
    """
    self.hdata = self.hdata.enrich_hyperedge_weights(enricher, enrichment_mode)

update_from_hdata(hdata)

Create a Dataset instance from an HData object.

Parameters:

Name Type Description Default
hdata HData

HData object containing the hypergraph data.

required

Returns:

Name Type Description
dataset Dataset

The Dataset instance with the provided HData.

Source code in hypertorch/data/dataset.py
def update_from_hdata(self, hdata: HData) -> Dataset:
    """
    Create a `Dataset` instance from an `HData` object.

    Args:
        hdata: `HData` object containing the hypergraph data.

    Returns:
        dataset: The `Dataset` instance with the provided `HData`.
    """
    return self.__class__(
        hdata=hdata,
        sampling_strategy=self.sampling_strategy,
        task=self.task,
    )

add_negative_samples(negative_sampler, seed=None)

Create a new Dataset with sampled negative hyperedges added.

Parameters:

Name Type Description Default
negative_sampler NegativeSampler

Sampler used to generate negative hyperedges from this dataset's hdata.

required
seed int | None

Optional random seed used for both negative sampling and the final shuffle. Defaults to None.

None

Returns:

Name Type Description
dataset Dataset

A new Dataset instance with positives and sampled negatives.

Source code in hypertorch/data/dataset.py
def add_negative_samples(
    self,
    negative_sampler: NegativeSampler,
    seed: int | None = None,
) -> Dataset:
    """
    Create a new Dataset with sampled negative hyperedges added.

    Args:
        negative_sampler: Sampler used to generate negative hyperedges from
            this dataset's ``hdata``.
        seed: Optional random seed used for both negative sampling and the final shuffle.
            Defaults to ``None``.

    Returns:
        dataset: A new Dataset instance with positives and sampled negatives.
    """
    hdata_with_negatives = self.hdata.clone()
    hdata_with_negatives = hdata_with_negatives.add_negative_samples(
        negative_sampler=negative_sampler,
        seed=seed,
    )
    return self.update_from_hdata(hdata_with_negatives)

remove_hyperedges_with_fewer_than_k_nodes(k, preserve_global_node_ids=False)

Remove hyperedges that have fewer than k incident nodes.

Parameters:

Name Type Description Default
k int

The minimum number of nodes a hyperedge must have to be retained.

required
preserve_global_node_ids bool

Whether to preserve the global node IDs after removing hyperedges. Defaults to False. If False, the global node IDs will be reindexed to be contiguous after removing hyperedges. If True, the global node IDs will be preserved, which may cause some models to raise as they may expect contiguous global node IDs.

False
Source code in hypertorch/data/dataset.py
def remove_hyperedges_with_fewer_than_k_nodes(
    self,
    k: int,
    preserve_global_node_ids: bool = False,
) -> None:
    """
    Remove hyperedges that have fewer than k incident nodes.

    Args:
        k: The minimum number of nodes a hyperedge must have to be retained.
        preserve_global_node_ids: Whether to preserve the global node IDs
            after removing hyperedges. Defaults to ``False``. If ``False``, the global node IDs
            will be reindexed to be contiguous after removing hyperedges.
            If ``True``, the global node IDs will be preserved, which may cause some models
            to raise as they may expect contiguous global node IDs.
    """
    self.hdata = self.hdata.remove_hyperedges_with_fewer_than_k_nodes(
        k, preserve_global_node_ids
    )

split(ratios=None, shuffle=False, seed=None, node_space_setting='transductive', sparse_split_hyperedges=False, cover_all_nodes_in_train_split=False, train_split_idx=0, splitter=None)

Split the dataset based on task into partitions: - For node classification, splits are based on nodes and their incident hyperedges. - For hyperlink prediction, splits are based on hyperedges and their incident nodes.

Boundaries are computed using cumulative floor to prevent early splits from over-consuming edges. The last split absorbs any rounding remainder. In the transductive setting, node-classification splits keep the full node space on the train split. Hyperlink-prediction splits keep the full hypergraph as context by default and mark supervised hyperedges with target_hyperedge_mask.

Splits that would end with zero hyperedges are rejected.

Use split_with_ratios to also get the final ratios after splitting.

Examples:

Transductive split:

>>> train, test = dataset.split([0.8, 0.2])
>>> train.hdata.num_nodes == dataset.hdata.num_nodes
>>> int(train.hdata.target_hyperedge_mask.sum().item()) == len(train)

Inductive split:

>>> train, test = dataset.split(
...     [0.8, 0.2],
...     node_space_setting="inductive",
... )
>>> train.hdata.num_nodes <= dataset.hdata.num_nodes
>>> test.hdata.num_nodes <= dataset.hdata.num_nodes

Parameters:

Name Type Description Default
ratios list[float] | None

List of floats summing to 1.0, e.g., [0.8, 0.1, 0.1].

None
shuffle bool | None

Whether to shuffle hyperedges before splitting. Defaults to False for deterministic splits.

False
seed int | None

Optional random seed for reproducibility. Ignored if shuffle is set to False.

None
node_space_setting NodeSpaceSetting

Whether to preserve the full node space in the splits. transductive (default) preserves the full node space on the first split. inductive keeps each split's local node space.

'transductive'
sparse_split_hyperedges bool

Whether hyperlink-prediction splits should use the sparse split behavior. Defaults to False, which keeps the full hypergraph as context in transductive splits and marks supervised hyperedges with target_hyperedge_mask.

False
cover_all_nodes_in_train_split bool

Whether a transductive sparse hyperedge split should move hyperedges from later splits until every node is incident to one of its selected hyperedges. Ratios are approximate when this coverage requires moving hyperedges into the first split.

False
train_split_idx int

The index of the split to treat as the train split. Defaults to 0, so the first split is the train split that is optionally rebalanced to cover all nodes in sparse transductive hyperlink-prediction splits. This is used only when node_space_setting=="transductive" and cover_all_nodes_in_train_split==True, to determine which split should be rebalanced to cover all nodes. For the 'inductive' setting, splits are always returned based on the provided ratios.

0
splitter Splitter[Dataset, Any] | None

Optional dataset splitter. When provided, it owns split construction and final-ratio reporting. Defaults to None.

None

Returns:

Name Type Description
datasets list[Dataset]

List of Dataset objects, one per split, each with contiguous IDs.

Raises:

Type Description
ValueError

If ratios do not sum to 1.0, a final split has zero hyperedges, if train coverage is requested for dense hyperlink-prediction splits, or a requested sparse train-cover split cannot cover the full node space.

Source code in hypertorch/data/dataset.py
def split(
    self,
    ratios: list[float] | None = None,
    shuffle: bool | None = False,
    seed: int | None = None,
    node_space_setting: NodeSpaceSetting = "transductive",
    sparse_split_hyperedges: bool = False,
    cover_all_nodes_in_train_split: bool = False,
    train_split_idx: int = 0,
    splitter: Splitter[Dataset, Any] | None = None,
) -> list[Dataset]:
    """
    Split the dataset based on task into partitions:
    - For node classification, splits are based on nodes and their incident hyperedges.
    - For hyperlink prediction, splits are based on hyperedges and their incident nodes.

    Boundaries are computed using cumulative floor to prevent early splits from
    over-consuming edges. The last split absorbs any rounding remainder.
    In the transductive setting, node-classification splits keep the full node space on
    the train split. Hyperlink-prediction splits keep the full hypergraph as context by
    default and mark supervised hyperedges with ``target_hyperedge_mask``.

    Splits that would end with zero hyperedges are rejected.

    Use ``split_with_ratios`` to also get the final ratios after splitting.

    Examples:
        Transductive split:
        >>> train, test = dataset.split([0.8, 0.2])
        >>> train.hdata.num_nodes == dataset.hdata.num_nodes
        >>> int(train.hdata.target_hyperedge_mask.sum().item()) == len(train)

        Inductive split:
        >>> train, test = dataset.split(
        ...     [0.8, 0.2],
        ...     node_space_setting="inductive",
        ... )
        >>> train.hdata.num_nodes <= dataset.hdata.num_nodes
        >>> test.hdata.num_nodes <= dataset.hdata.num_nodes

    Args:
        ratios: List of floats summing to ``1.0``, e.g., ``[0.8, 0.1, 0.1]``.
        shuffle: Whether to shuffle hyperedges before splitting.
            Defaults to ``False`` for deterministic splits.
        seed: Optional random seed for reproducibility. Ignored if shuffle is set to ``False``.
        node_space_setting: Whether to preserve the full node space in the splits.
            ``transductive`` (default) preserves the full node space on the
            first split. ``inductive`` keeps each split's local node space.
        sparse_split_hyperedges: Whether hyperlink-prediction splits should use the
            sparse split behavior. Defaults to ``False``, which keeps the full
            hypergraph as context in transductive splits and marks supervised hyperedges with
            ``target_hyperedge_mask``.
        cover_all_nodes_in_train_split: Whether a transductive sparse hyperedge split
            should move hyperedges from later splits until every node is
            incident to one of its selected hyperedges. Ratios are approximate
            when this coverage requires moving hyperedges into the first split.
        train_split_idx: The index of the split to treat as the train split. Defaults to ``0``,
            so the first split is the train split that is optionally rebalanced to cover
            all nodes in sparse transductive hyperlink-prediction splits.
            This is used only when ``node_space_setting=="transductive"``
            and ``cover_all_nodes_in_train_split==True``,
            to determine which split should be rebalanced to cover all nodes.
            For the 'inductive' setting, splits are always returned based on
            the provided ratios.
        splitter: Optional dataset splitter. When provided, it owns split
            construction and final-ratio reporting. Defaults to ``None``.

    Returns:
        datasets: List of Dataset objects, one per split, each with contiguous IDs.

    Raises:
        ValueError: If ratios do not sum to ``1.0``, a final split has zero
            hyperedges, if train coverage is requested for dense hyperlink-prediction
            splits, or a requested sparse train-cover split cannot cover the full node
            space.
    """
    if splitter is not None:
        return splitter.split(self)

    if ratios is None:
        raise ValueError("'ratios' must be provided when no custom 'splitter' is provided.")

    if self.hdata.is_node_related_task:
        splits, _ = NodeDatasetSplitter(
            node_space_setting=node_space_setting,
            shuffle=shuffle,
            seed=seed,
        ).split(
            to_split=self,
            ratios=ratios,
        )
        return splits

    if not self.hdata.is_hyperedge_related_task:
        raise ValueError(f"Unsupported task category for task={self.hdata.task!r}.")

    hyperedge_splitter_cls = (
        SparseHyperedgeDatasetSplitter if sparse_split_hyperedges else HyperedgeDatasetSplitter
    )
    splits, _ = hyperedge_splitter_cls(
        node_space_setting=node_space_setting,
        shuffle=shuffle,
        seed=seed,
    ).split(
        to_split=self,
        ratios=ratios,
        train_split_idx=train_split_idx,
        cover_all_nodes_in_train_split=cover_all_nodes_in_train_split,
    )
    return splits

split_with_ratios(ratios, shuffle=False, seed=None, node_space_setting='transductive', cover_all_nodes_in_train_split=False, train_split_idx=0, sparse_split_hyperedges=False)

Split the dataset based on task and return the final hyperedge ratios: - For node classification, splits are based on nodes and their incident hyperedges. For more details, look at the NodeDatasetSplitter class. - For hyperlink prediction, splits are based on hyperedges and their incident nodes. For more details, look at the HyperedgeDatasetSplitter class.

Boundaries are computed using cumulative floor to prevent early splits from over-consuming edges. The last split absorbs any rounding remainder. In the transductive setting, node-classification splits keep the full node space on the train split. Hyperlink-prediction splits keep the full hypergraph as context by default and mark supervised hyperedges with target_hyperedge_mask.

Splits that would end with zero hyperedges are rejected.

Final ratios are computed from split hyperedge counts after ratio boundaries and any requested transductive rebalancing have been applied.

To provide a custom splitting implementation, use the splitter argument of the split method instead.

Parameters:

Name Type Description Default
ratios list[float]

List of floats summing to 1.0, e.g., [0.8, 0.1, 0.1].

required
shuffle bool | None

Whether to shuffle hyperedges before splitting. Defaults to False for deterministic splits.

False
node_space_setting NodeSpaceSetting

Whether to preserve the full node space in the splits. transductive (default) preserves the full node space on the first split. inductive keeps each split's local node space.

'transductive'
sparse_split_hyperedges bool

Whether hyperlink-prediction splits should use the legacy sparse materialization behavior. Defaults to False, which keeps the full hypergraph as context in transductive splits and marks supervised hyperedges with target_hyperedge_mask.

False
cover_all_nodes_in_train_split bool

Whether a transductive sparse hyperedge split should move hyperedges from later splits until every node is incident to one of its selected hyperedges.

False
train_split_idx int

The index of the split to treat as the train split. Defaults to 0, so the first split is the train split that is optionally rebalanced to cover all nodes in sparse transductive hyperlink-prediction splits. This is used only when node_space_setting=="transductive" and cover_all_nodes_in_train_split==True, to determine which split should be rebalanced to cover all nodes. For the 'inductive' setting, splits are always returned based on the provided ratios.

0
seed int | None

Optional random seed for reproducibility. Ignored if shuffle is set to False. Defaults to None.

None

Returns:

Name Type Description
datasets list[Dataset]

List of Dataset instances, one per split, each with contiguous IDs.

final_ratios list[float]

List of floats representing the actual ratios of target hyperedges in each split after splitting and any requested rebalancing.

Raises:

Type Description
ValueError

If ratios do not sum to 1.0, a final split has zero hyperedges, or a requested transductive train-cover split cannot cover the full node space.

Source code in hypertorch/data/dataset.py
def split_with_ratios(
    self,
    ratios: list[float],
    shuffle: bool | None = False,
    seed: int | None = None,
    node_space_setting: NodeSpaceSetting = "transductive",
    cover_all_nodes_in_train_split: bool = False,
    train_split_idx: int = 0,
    sparse_split_hyperedges: bool = False,
) -> tuple[list[Dataset], list[float]]:
    """
    Split the dataset based on task and return the final hyperedge ratios:
    - For node classification, splits are based on nodes and their incident hyperedges.
      For more details, look at the `NodeDatasetSplitter` class.
    - For hyperlink prediction, splits are based on hyperedges and their incident nodes.
      For more details, look at the `HyperedgeDatasetSplitter` class.

    Boundaries are computed using cumulative floor to prevent early splits from
    over-consuming edges. The last split absorbs any rounding remainder.
    In the transductive setting, node-classification splits keep the full node space on
    the train split. Hyperlink-prediction splits keep the full hypergraph as context by
    default and mark supervised hyperedges with ``target_hyperedge_mask``.

    Splits that would end with zero hyperedges are rejected.

    Final ratios are computed from split hyperedge counts after ratio
    boundaries and any requested transductive rebalancing have been applied.

    To provide a custom splitting implementation, use the ``splitter``
    argument of the ``split`` method instead.

    Args:
        ratios: List of floats summing to ``1.0``, e.g., ``[0.8, 0.1, 0.1]``.
        shuffle: Whether to shuffle hyperedges before splitting. Defaults to
            ``False`` for deterministic splits.
        node_space_setting: Whether to preserve the full node space in the
            splits. ``transductive`` (default) preserves the full node space
            on the first split. ``inductive`` keeps each split's local node space.
        sparse_split_hyperedges: Whether hyperlink-prediction splits should use the legacy
            sparse materialization behavior. Defaults to ``False``, which keeps the full
            hypergraph as context in transductive splits and marks supervised hyperedges
            with ``target_hyperedge_mask``.
        cover_all_nodes_in_train_split: Whether a transductive sparse hyperedge split
            should move hyperedges from later splits until every node is
            incident to one of its selected hyperedges.
        train_split_idx: The index of the split to treat as the train split. Defaults to ``0``,
            so the first split is the train split that is optionally rebalanced to cover
            all nodes in sparse transductive hyperlink-prediction splits.
            This is used only when ``node_space_setting=="transductive"``
            and ``cover_all_nodes_in_train_split==True``,
            to determine which split should be rebalanced to cover all nodes.
            For the 'inductive' setting, splits are always returned based on
            the provided ratios.
        seed: Optional random seed for reproducibility. Ignored if ``shuffle``
            is set to ``False``. Defaults to ``None``.

    Returns:
        datasets: List of Dataset instances, one per split, each with contiguous IDs.
        final_ratios: List of floats representing the actual ratios of target hyperedges
            in each split after splitting and any requested rebalancing.

    Raises:
        ValueError: If ratios do not sum to ``1.0``, a final split has zero
            hyperedges, or a requested transductive train-cover split cannot
            cover the full node space.
    """
    if self.hdata.is_node_related_task:
        return NodeDatasetSplitter(
            node_space_setting=node_space_setting,
            shuffle=shuffle,
            seed=seed,
        ).split(
            to_split=self,
            ratios=ratios,
        )

    if not self.hdata.is_hyperedge_related_task:
        raise ValueError(f"Unsupported task category for task={self.hdata.task!r}.")

    hyperedge_splitter_cls = (
        SparseHyperedgeDatasetSplitter if sparse_split_hyperedges else HyperedgeDatasetSplitter
    )
    return hyperedge_splitter_cls(
        node_space_setting=node_space_setting,
        shuffle=shuffle,
        seed=seed,
    ).split(
        to_split=self,
        ratios=ratios,
        train_split_idx=train_split_idx,
        cover_all_nodes_in_train_split=cover_all_nodes_in_train_split,
    )

to(device)

Move the dataset's HData to the specified device.

Parameters:

Name Type Description Default
device device

The target device (e.g., torch.device('cuda') or torch.device('cpu')).

required

Returns:

Name Type Description
dataset Dataset

The Dataset instance moved to the specified device.

Source code in hypertorch/data/dataset.py
def to(self, device: torch.device) -> Dataset:
    """
    Move the dataset's HData to the specified device.

    Args:
        device: The target device (e.g., ``torch.device('cuda')`` or ``torch.device('cpu')``).

    Returns:
        dataset: The Dataset instance moved to the specified device.
    """
    self.hdata = self.hdata.to(device)
    return self

transform_node_attrs(attrs, attr_keys=None)

Transform HIF node attributes into a numeric tensor. Overload this in case processing node attributes requires custom logic.

Parameters:

Name Type Description Default
attrs dict[str, Any]

Attributes to transform.

required
attr_keys list[str] | None

Optional attribute key order used for consistent output shape. Defaults to None.

None

Returns:

Name Type Description
attrs Tensor

Tensor containing numeric attribute values.

Source code in hypertorch/data/dataset.py
def transform_node_attrs(
    self,
    attrs: dict[str, Any],
    attr_keys: list[str] | None = None,
) -> Tensor:
    """
    Transform HIF node attributes into a numeric tensor.
    Overload this in case processing node attributes requires custom logic.

    Args:
        attrs: Attributes to transform.
        attr_keys: Optional attribute key order used for consistent output shape.
            Defaults to ``None``.

    Returns:
        attrs: Tensor containing numeric attribute values.
    """
    return HIFProcessor.transform_attrs(attrs, attr_keys)

transform_hyperedge_attrs(attrs, attr_keys=None)

Transform hyperedge attributes into a numeric tensor. Overload this in case processing hyperedge attributes requires custom logic.

Parameters:

Name Type Description Default
attrs dict[str, Any]

Attributes to transform.

required
attr_keys list[str] | None

Optional attribute key order used for consistent output shape. Defaults to None.

None

Returns:

Name Type Description
attrs Tensor

Tensor containing numeric attribute values.

Source code in hypertorch/data/dataset.py
def transform_hyperedge_attrs(
    self,
    attrs: dict[str, Any],
    attr_keys: list[str] | None = None,
) -> Tensor:
    """
    Transform hyperedge attributes into a numeric tensor.
    Overload this in case processing hyperedge attributes requires custom logic.

    Args:
        attrs: Attributes to transform.
        attr_keys: Optional attribute key order used for consistent output shape.
            Defaults to ``None``.

    Returns:
        attrs: Tensor containing numeric attribute values.
    """
    return HIFProcessor.transform_attrs(attrs, attr_keys)

stats()

Compute statistics for the dataset.

This method currently delegates to the underlying HData's stats method.

Fields
  • shape_x: The shape of the node feature matrix x.
  • shape_hyperedge_attr: The shape of the hyperedge attribute matrix, or None if hyperedge attributes are not present.
  • num_nodes: The number of nodes in the hypergraph.
  • num_hyperedges: The number of hyperedges in the hypergraph.
  • avg_degree_node_raw: The average degree of nodes, calculated as the mean number of hyperedges each node belongs to.
  • avg_degree_node: The floored node average degree.
  • avg_degree_hyperedge_raw: The average size of hyperedges, calculated as the mean number of nodes each hyperedge contains.
  • avg_degree_hyperedge: The floored hyperedge average size.
  • node_degree_max: The maximum degree of any node in the hypergraph.
  • hyperedge_degree_max: The maximum size of any hyperedge in the hypergraph.
  • node_degree_median: The median degree of nodes in the hypergraph.
  • hyperedge_degree_median: The median size of hyperedges in the hypergraph.
  • distribution_node_degree: A list where the value at index i represents the count of nodes with degree i.
  • distribution_hyperedge_size: A list where the value at index i represents the count of hyperedges with size i.
  • distribution_node_degree_hist: A dictionary where the keys are node degrees and the values are the count of nodes with that degree.
  • distribution_hyperedge_size_hist: A dictionary where the keys are hyperedge sizes and the values are the count of hyperedges with that size.

Returns:

Name Type Description
stats dict[str, Any]

A dictionary containing various statistics about the hypergraph.

Source code in hypertorch/data/dataset.py
def stats(self) -> dict[str, Any]:
    """
    Compute statistics for the dataset.

    This method currently delegates to the underlying HData's stats method.

    Fields:
        - ``shape_x``: The shape of the node feature matrix ``x``.
        - ``shape_hyperedge_attr``: The shape of the hyperedge attribute matrix, or ``None``
        if hyperedge attributes are not present.
        - ``num_nodes``: The number of nodes in the hypergraph.
        - ``num_hyperedges``: The number of hyperedges in the hypergraph.
        - ``avg_degree_node_raw``: The average degree of nodes, calculated as the mean number
         of hyperedges each node belongs to.
        - ``avg_degree_node``: The floored node average degree.
        - ``avg_degree_hyperedge_raw``: The average size of hyperedges, calculated as the
        mean number of nodes each hyperedge contains.
        - ``avg_degree_hyperedge``: The floored hyperedge average size.
        - ``node_degree_max``: The maximum degree of any node in the hypergraph.
        - ``hyperedge_degree_max``: The maximum size of any hyperedge in the hypergraph.
        - ``node_degree_median``: The median degree of nodes in the hypergraph.
        - ``hyperedge_degree_median``: The median size of hyperedges in the hypergraph.
        - ``distribution_node_degree``: A list where the value at index ``i`` represents
        the count of nodes with degree ``i``.
        - ``distribution_hyperedge_size``: A list where the value at index ``i`` represents
        the count of hyperedges with size ``i``.
        - ``distribution_node_degree_hist``: A dictionary where the keys are node degrees
         and the values are the count of nodes with that degree.
        - ``distribution_hyperedge_size_hist``: A dictionary where the keys are hyperedge
         sizes and the values are the count of hyperedges with that size.

    Returns:
        stats: A dictionary containing various statistics about the hypergraph.
    """
    return self.hdata.stats()

HIFLoader

A utility class to load hypergraphs from HIF format.

Source code in hypertorch/data/hif.py
class HIFLoader:
    """
    A utility class to load hypergraphs from HIF format.
    """

    @classmethod
    def load_from_url(
        cls,
        url: str,
        task: Task = TaskEnum.HYPERLINK_PREDICTION,
        save_on_disk: bool = False,
    ) -> HData:
        """
        Load a hypergraph from a given URL pointing to a .json or .json.zst file in HIF format.

        Args:
            url: The URL to the .json or .json.zst file containing the HIF hypergraph data.
            task: The learning task for the loaded hypergraph.
            save_on_disk (bool): Whether to save the downloaded file on disk.

        Returns:
            hdata: The loaded hypergraph object.

        Raises:
            ValueError: If the URL cannot be downloaded, has an unsupported file format,
                or has an unexpected filename format.
        """
        url = validate_http_url(url)

        response = requests.get(url, timeout=20)
        if response.status_code != 200:
            raise ValueError(
                f"Failed to download dataset from URL {url!r} "
                f"with status code {response.status_code}"
            )

        if not url.endswith((".json.zst", ".json")):
            raise ValueError(
                f"Unsupported file format for URL {url!r}. Expected .json or .json.zst"
            )

        if os.path.basename(url).count(".") > 2:
            raise ValueError(
                f"URL {url!r} has an unexpected filename format. "
                "Expected at most one dot in the base filename before the "
                "extension (e.g., dataset.json or dataset.json.zst)."
            )

        if url.endswith(".json.zst"):
            hif_data = from_zst_bytes_to_json(response.content)
            hdata = cls.__process_hif_data(hif_data=hif_data, task=task)
            if save_on_disk:
                write_dataset_to_disk_as_zst(
                    dataset_name=os.path.basename(url), content=response.content
                )
        else:  # json
            hif_data = from_bytes_to_json(response.content)
            hdata = cls.__process_hif_data(hif_data=hif_data, task=task)
            if save_on_disk:
                compressed_hif_data = compress_json_bytes_as_zst(response.content)

                write_dataset_to_disk_as_zst(
                    dataset_name=os.path.basename(url), content=compressed_hif_data
                )

        return hdata

    @classmethod
    def load_from_path(cls, filepath: str, task: Task = TaskEnum.HYPERLINK_PREDICTION) -> HData:
        """
        Load a hypergraph from a local file path pointing to a .json or .json.zst file in HIF
        format.

        Args:
            filepath: The local file path to the .json or .json.zst file
                containing the HIF hypergraph data.
            task: The learning task for the loaded hypergraph.

        Returns:
            hdata: The loaded hypergraph object.

        Raises:
            ValueError: If ``filepath`` does not exist or has an unsupported file format.
        """
        if not os.path.exists(filepath):
            raise ValueError(f"File {filepath!r} does not exist.")

        if filepath.endswith(".zst"):
            hif_data = from_zst_file_to_json(filepath)
        elif filepath.endswith(".json"):
            hif_data = from_file_to_json(filepath)
        else:
            raise ValueError(
                f"Unsupported format for file {filepath!r}. Expected .json or .json.zst"
            )

        return cls.__process_hif_data(hif_data=hif_data, task=task)

    @classmethod
    def load_by_name(
        cls,
        dataset_name: str,
        hf_sha: str | None = None,
        task: Task = TaskEnum.HYPERLINK_PREDICTION,
        save_on_disk: bool = False,
    ) -> HData:
        """
        Load a supported dataset by name.

        Args:
            dataset_name: Name of the dataset to load.
            task: Task type for the dataset. Defaults to "hyperlink-prediction".
            hf_sha: Optional pinned Hugging Face revision used as a fallback source.
            save_on_disk: Whether to cache the downloaded compressed dataset file.
                Defaults to ``False``.

        Returns:
            hdata: Loaded hypergraph data.

        Raises:
            ValueError: If the dataset cannot be downloaded or parsed.
        """
        cache_dir = get_cache_dir()
        output_dir = os.path.join(cache_dir, "datasets")
        zst_filename = os.path.join(output_dir, f"{dataset_name}.json.zst")
        repo_root = get_cache_dir()
        hf_cache_dir = os.path.join(repo_root, "hf_cache")
        if os.path.exists(zst_filename):
            hif_data = from_zst_file_to_json(zst_filename)
            return cls.__process_hif_data(hif_data=hif_data, dataset_name=dataset_name, task=task)

        github_url = (
            f"https://raw.githubusercontent.com/hypernetwork-research-group/datasets/"
            f"{GITHUB_COMMIT_SHA}/{dataset_name}.json.zst"
        )
        response = requests.get(github_url, timeout=20)
        if response.status_code == 200:
            dataset_bytes = response.content
            hif_data = from_zst_bytes_to_json(dataset_bytes)
            hdata = cls.__process_hif_data(hif_data=hif_data, dataset_name=dataset_name, task=task)
            if save_on_disk:
                write_zst_file_to_disk(zst_filename=zst_filename, content=dataset_bytes)
            return hdata

        warnings.warn(
            f"GitHub raw download failed for dataset {dataset_name!r} "
            f"with status code {response.status_code}\n"
            "Falling back to Hugging Face Hub download for dataset",
            category=UserWarning,
            stacklevel=2,
        )

        if hf_sha is None:
            raise ValueError(
                f"Failed to download dataset {dataset_name!r} from GitHub "
                f"with status code {response.status_code} "
                f"and no SHA provided for Hugging Face Hub fallback."
            )

        try:
            token: str | None = os.getenv("HF_DOWNLOAD_TOKEN")
            token = token.strip() if token and token.strip() else None
            downloaded_path = hf_hub_download(
                repo_id=f"HypernetworkRG/{dataset_name}",
                filename=f"{dataset_name}.json.zst",
                repo_type="dataset",
                revision=hf_sha,
                cache_dir=hf_cache_dir,
                token=token,
            )
        except Exception as e:
            raise ValueError(
                f"Failed to download dataset {dataset_name!r} from GitHub and Hugging Face Hub. "
                f"GitHub error: {response.status_code} | Hugging Face error: {e!s}."
            ) from e

        hif_data = from_zst_file_to_json(downloaded_path)
        hdata = cls.__process_hif_data(hif_data=hif_data, dataset_name=dataset_name, task=task)
        if save_on_disk:
            try:
                os.makedirs(os.path.dirname(zst_filename), exist_ok=True)
                shutil.copyfile(downloaded_path, zst_filename)
            except Exception as e:
                raise ValueError(
                    f"Failed to save downloaded dataset {dataset_name!r} to disk at "
                    f"{zst_filename!r}: {e!s}."
                ) from e

        if os.path.isdir(hf_cache_dir):
            try:
                path_prefix = f"datasets--HypernetworkRG--{dataset_name}"
                shutil.rmtree(os.path.join(hf_cache_dir, path_prefix))
                shutil.rmtree(os.path.join(hf_cache_dir, ".locks", path_prefix))
            except Exception as e:
                warnings.warn(
                    f"Failed to clean up Hugging Face Hub cache after downloading "
                    f"dataset {dataset_name!r}: {e!s}.",
                    category=UserWarning,
                    stacklevel=2,
                )
        return hdata

    @classmethod
    def __process_hif_data(
        cls,
        hif_data: dict[str, Any],
        dataset_name: str | None = None,
        task: Task = TaskEnum.HYPERLINK_PREDICTION,
    ) -> HData:
        """
        Validate and process parsed HIF data.

        Args:
            hif_data: Parsed HIF JSON data.
            dataset_name: Optional dataset name used in validation errors.
            task: Task type for the dataset. Defaults to "hyperlink-prediction".

        Returns:
            hdata: Processed hypergraph data.

        Raises:
            ValueError: If the data is not HIF-compliant.
        """
        if not validate_hif_data(hif_data):
            raise ValueError(f"Dataset {dataset_name or ''} is not HIF-compliant.")

        hypergraph = HIFHypergraph.from_hif(hif_data)
        return HIFProcessor.process_hypergraph(hypergraph=hypergraph, task=task)

load_from_url(url, task=TaskEnum.HYPERLINK_PREDICTION, save_on_disk=False) classmethod

Load a hypergraph from a given URL pointing to a .json or .json.zst file in HIF format.

Parameters:

Name Type Description Default
url str

The URL to the .json or .json.zst file containing the HIF hypergraph data.

required
task Task

The learning task for the loaded hypergraph.

HYPERLINK_PREDICTION
save_on_disk bool

Whether to save the downloaded file on disk.

False

Returns:

Name Type Description
hdata HData

The loaded hypergraph object.

Raises:

Type Description
ValueError

If the URL cannot be downloaded, has an unsupported file format, or has an unexpected filename format.

Source code in hypertorch/data/hif.py
@classmethod
def load_from_url(
    cls,
    url: str,
    task: Task = TaskEnum.HYPERLINK_PREDICTION,
    save_on_disk: bool = False,
) -> HData:
    """
    Load a hypergraph from a given URL pointing to a .json or .json.zst file in HIF format.

    Args:
        url: The URL to the .json or .json.zst file containing the HIF hypergraph data.
        task: The learning task for the loaded hypergraph.
        save_on_disk (bool): Whether to save the downloaded file on disk.

    Returns:
        hdata: The loaded hypergraph object.

    Raises:
        ValueError: If the URL cannot be downloaded, has an unsupported file format,
            or has an unexpected filename format.
    """
    url = validate_http_url(url)

    response = requests.get(url, timeout=20)
    if response.status_code != 200:
        raise ValueError(
            f"Failed to download dataset from URL {url!r} "
            f"with status code {response.status_code}"
        )

    if not url.endswith((".json.zst", ".json")):
        raise ValueError(
            f"Unsupported file format for URL {url!r}. Expected .json or .json.zst"
        )

    if os.path.basename(url).count(".") > 2:
        raise ValueError(
            f"URL {url!r} has an unexpected filename format. "
            "Expected at most one dot in the base filename before the "
            "extension (e.g., dataset.json or dataset.json.zst)."
        )

    if url.endswith(".json.zst"):
        hif_data = from_zst_bytes_to_json(response.content)
        hdata = cls.__process_hif_data(hif_data=hif_data, task=task)
        if save_on_disk:
            write_dataset_to_disk_as_zst(
                dataset_name=os.path.basename(url), content=response.content
            )
    else:  # json
        hif_data = from_bytes_to_json(response.content)
        hdata = cls.__process_hif_data(hif_data=hif_data, task=task)
        if save_on_disk:
            compressed_hif_data = compress_json_bytes_as_zst(response.content)

            write_dataset_to_disk_as_zst(
                dataset_name=os.path.basename(url), content=compressed_hif_data
            )

    return hdata

load_from_path(filepath, task=TaskEnum.HYPERLINK_PREDICTION) classmethod

Load a hypergraph from a local file path pointing to a .json or .json.zst file in HIF format.

Parameters:

Name Type Description Default
filepath str

The local file path to the .json or .json.zst file containing the HIF hypergraph data.

required
task Task

The learning task for the loaded hypergraph.

HYPERLINK_PREDICTION

Returns:

Name Type Description
hdata HData

The loaded hypergraph object.

Raises:

Type Description
ValueError

If filepath does not exist or has an unsupported file format.

Source code in hypertorch/data/hif.py
@classmethod
def load_from_path(cls, filepath: str, task: Task = TaskEnum.HYPERLINK_PREDICTION) -> HData:
    """
    Load a hypergraph from a local file path pointing to a .json or .json.zst file in HIF
    format.

    Args:
        filepath: The local file path to the .json or .json.zst file
            containing the HIF hypergraph data.
        task: The learning task for the loaded hypergraph.

    Returns:
        hdata: The loaded hypergraph object.

    Raises:
        ValueError: If ``filepath`` does not exist or has an unsupported file format.
    """
    if not os.path.exists(filepath):
        raise ValueError(f"File {filepath!r} does not exist.")

    if filepath.endswith(".zst"):
        hif_data = from_zst_file_to_json(filepath)
    elif filepath.endswith(".json"):
        hif_data = from_file_to_json(filepath)
    else:
        raise ValueError(
            f"Unsupported format for file {filepath!r}. Expected .json or .json.zst"
        )

    return cls.__process_hif_data(hif_data=hif_data, task=task)

load_by_name(dataset_name, hf_sha=None, task=TaskEnum.HYPERLINK_PREDICTION, save_on_disk=False) classmethod

Load a supported dataset by name.

Parameters:

Name Type Description Default
dataset_name str

Name of the dataset to load.

required
task Task

Task type for the dataset. Defaults to "hyperlink-prediction".

HYPERLINK_PREDICTION
hf_sha str | None

Optional pinned Hugging Face revision used as a fallback source.

None
save_on_disk bool

Whether to cache the downloaded compressed dataset file. Defaults to False.

False

Returns:

Name Type Description
hdata HData

Loaded hypergraph data.

Raises:

Type Description
ValueError

If the dataset cannot be downloaded or parsed.

Source code in hypertorch/data/hif.py
@classmethod
def load_by_name(
    cls,
    dataset_name: str,
    hf_sha: str | None = None,
    task: Task = TaskEnum.HYPERLINK_PREDICTION,
    save_on_disk: bool = False,
) -> HData:
    """
    Load a supported dataset by name.

    Args:
        dataset_name: Name of the dataset to load.
        task: Task type for the dataset. Defaults to "hyperlink-prediction".
        hf_sha: Optional pinned Hugging Face revision used as a fallback source.
        save_on_disk: Whether to cache the downloaded compressed dataset file.
            Defaults to ``False``.

    Returns:
        hdata: Loaded hypergraph data.

    Raises:
        ValueError: If the dataset cannot be downloaded or parsed.
    """
    cache_dir = get_cache_dir()
    output_dir = os.path.join(cache_dir, "datasets")
    zst_filename = os.path.join(output_dir, f"{dataset_name}.json.zst")
    repo_root = get_cache_dir()
    hf_cache_dir = os.path.join(repo_root, "hf_cache")
    if os.path.exists(zst_filename):
        hif_data = from_zst_file_to_json(zst_filename)
        return cls.__process_hif_data(hif_data=hif_data, dataset_name=dataset_name, task=task)

    github_url = (
        f"https://raw.githubusercontent.com/hypernetwork-research-group/datasets/"
        f"{GITHUB_COMMIT_SHA}/{dataset_name}.json.zst"
    )
    response = requests.get(github_url, timeout=20)
    if response.status_code == 200:
        dataset_bytes = response.content
        hif_data = from_zst_bytes_to_json(dataset_bytes)
        hdata = cls.__process_hif_data(hif_data=hif_data, dataset_name=dataset_name, task=task)
        if save_on_disk:
            write_zst_file_to_disk(zst_filename=zst_filename, content=dataset_bytes)
        return hdata

    warnings.warn(
        f"GitHub raw download failed for dataset {dataset_name!r} "
        f"with status code {response.status_code}\n"
        "Falling back to Hugging Face Hub download for dataset",
        category=UserWarning,
        stacklevel=2,
    )

    if hf_sha is None:
        raise ValueError(
            f"Failed to download dataset {dataset_name!r} from GitHub "
            f"with status code {response.status_code} "
            f"and no SHA provided for Hugging Face Hub fallback."
        )

    try:
        token: str | None = os.getenv("HF_DOWNLOAD_TOKEN")
        token = token.strip() if token and token.strip() else None
        downloaded_path = hf_hub_download(
            repo_id=f"HypernetworkRG/{dataset_name}",
            filename=f"{dataset_name}.json.zst",
            repo_type="dataset",
            revision=hf_sha,
            cache_dir=hf_cache_dir,
            token=token,
        )
    except Exception as e:
        raise ValueError(
            f"Failed to download dataset {dataset_name!r} from GitHub and Hugging Face Hub. "
            f"GitHub error: {response.status_code} | Hugging Face error: {e!s}."
        ) from e

    hif_data = from_zst_file_to_json(downloaded_path)
    hdata = cls.__process_hif_data(hif_data=hif_data, dataset_name=dataset_name, task=task)
    if save_on_disk:
        try:
            os.makedirs(os.path.dirname(zst_filename), exist_ok=True)
            shutil.copyfile(downloaded_path, zst_filename)
        except Exception as e:
            raise ValueError(
                f"Failed to save downloaded dataset {dataset_name!r} to disk at "
                f"{zst_filename!r}: {e!s}."
            ) from e

    if os.path.isdir(hf_cache_dir):
        try:
            path_prefix = f"datasets--HypernetworkRG--{dataset_name}"
            shutil.rmtree(os.path.join(hf_cache_dir, path_prefix))
            shutil.rmtree(os.path.join(hf_cache_dir, ".locks", path_prefix))
        except Exception as e:
            warnings.warn(
                f"Failed to clean up Hugging Face Hub cache after downloading "
                f"dataset {dataset_name!r}: {e!s}.",
                category=UserWarning,
                stacklevel=2,
            )
    return hdata

__process_hif_data(hif_data, dataset_name=None, task=TaskEnum.HYPERLINK_PREDICTION) classmethod

Validate and process parsed HIF data.

Parameters:

Name Type Description Default
hif_data dict[str, Any]

Parsed HIF JSON data.

required
dataset_name str | None

Optional dataset name used in validation errors.

None
task Task

Task type for the dataset. Defaults to "hyperlink-prediction".

HYPERLINK_PREDICTION

Returns:

Name Type Description
hdata HData

Processed hypergraph data.

Raises:

Type Description
ValueError

If the data is not HIF-compliant.

Source code in hypertorch/data/hif.py
@classmethod
def __process_hif_data(
    cls,
    hif_data: dict[str, Any],
    dataset_name: str | None = None,
    task: Task = TaskEnum.HYPERLINK_PREDICTION,
) -> HData:
    """
    Validate and process parsed HIF data.

    Args:
        hif_data: Parsed HIF JSON data.
        dataset_name: Optional dataset name used in validation errors.
        task: Task type for the dataset. Defaults to "hyperlink-prediction".

    Returns:
        hdata: Processed hypergraph data.

    Raises:
        ValueError: If the data is not HIF-compliant.
    """
    if not validate_hif_data(hif_data):
        raise ValueError(f"Dataset {dataset_name or ''} is not HIF-compliant.")

    hypergraph = HIFHypergraph.from_hif(hif_data)
    return HIFProcessor.process_hypergraph(hypergraph=hypergraph, task=task)

HIFProcessor

A utility class to process HIF hypergraph data into HData format.

Source code in hypertorch/data/hif.py
class HIFProcessor:
    """
    A utility class to process HIF hypergraph data into `HData` format.
    """

    @staticmethod
    def transform_attrs(
        attrs: dict[str, Any],
        attr_keys: list[str] | None = None,
    ) -> Tensor:
        """
        Extract and encode numeric attributes to tensor.

        Non-numeric attributes are discarded. Missing attributes are filled with ``0.0``.

        Args:
            attrs: Dictionary of attributes
            attr_keys: Optional list of attribute keys to encode. If provided,
                ensures consistent ordering and fill missing with ``0.0``.
                Defaults to ``None``.

        Returns:
            attrs: Tensor of numeric attribute values
        """
        numeric_attrs = {
            key: value
            for key, value in attrs.items()
            if isinstance(value, (int, float)) and not isinstance(value, bool)
        }

        if attr_keys is not None:
            values = [float(numeric_attrs.get(key, 0.0)) for key in attr_keys]
            return torch.tensor(values, dtype=torch.float)

        if not numeric_attrs:
            return torch.tensor([], dtype=torch.float)

        values = [float(value) for value in numeric_attrs.values()]
        return torch.tensor(values, dtype=torch.float)

    @classmethod
    def process_hypergraph(
        cls,
        hypergraph: HIFHypergraph,
        task: Task = TaskEnum.HYPERLINK_PREDICTION,
    ) -> HData:
        """
        Process the loaded hypergraph into `HData` format, mapping HIF structure to tensors.

        Returns:
            hdata: The processed hypergraph data.

        Raises:
            ValueError: If HIF node IDs are not unique or an incidence references an
                undeclared node ID.
        """
        num_nodes = len(hypergraph.nodes)
        x = cls.__process_x(hypergraph, num_nodes)

        # Remap node IDs to 0-based contiguous IDs (using indices) matching the x tensor order
        node_id_to_idx = {node.get("node"): idx for idx, node in enumerate(hypergraph.nodes)}
        if len(node_id_to_idx) != num_nodes:
            raise ValueError("HIF node IDs must be unique.")

        # Initialize edge_set only with edges that have incidences, so that
        # we avoid inflating edge count due to isolated nodes/missing incidences
        hyperedge_id_to_idx: dict[Any, int] = {}

        node_ids = []
        hyperedge_ids = []
        nodes_with_incidences = set()
        for incidence in hypergraph.incidences:
            node_id = incidence.get("node", 0)
            hyperedge_id = incidence.get("edge", 0)
            if node_id not in node_id_to_idx:
                raise ValueError(
                    f"Incidence references unknown node id {node_id!r}; "
                    "all incidence nodes must be declared in the HIF nodes list."
                )

            if hyperedge_id not in hyperedge_id_to_idx:
                # Hyperedges start from 0 and are assigned IDs in the order they are
                # first encountered in incidences
                hyperedge_id_to_idx[hyperedge_id] = len(hyperedge_id_to_idx)

            node_ids.append(node_id_to_idx[node_id])
            hyperedge_ids.append(hyperedge_id_to_idx[hyperedge_id])
            nodes_with_incidences.add(node_id_to_idx[node_id])

        # Handle isolated nodes by assigning them to a new unique hyperedge (self-loop)
        for node_idx in range(num_nodes):
            if node_idx not in nodes_with_incidences:
                new_hyperedge_id = len(hyperedge_id_to_idx)
                # Unique dummy key to reserve the index in hyperedge_set
                hyperedge_id_to_idx[f"__self_loop_{node_idx}__"] = new_hyperedge_id
                node_ids.append(node_idx)
                hyperedge_ids.append(new_hyperedge_id)

        num_hyperedges = len(hyperedge_id_to_idx)
        hyperedge_attr = cls.__process_hyperedge_attr(
            hypergraph=hypergraph,
            hyperedge_id_to_idx=hyperedge_id_to_idx,
            num_hyperedges=num_hyperedges,
        )

        hyperedge_weights = cls.__process_hyperedge_weights(
            hypergraph=hypergraph,
            hyperedge_id_to_idx=hyperedge_id_to_idx,
            num_hyperedges=num_hyperedges,
        )

        hyperedge_index = torch.tensor([node_ids, hyperedge_ids], dtype=torch.long)

        return HData(
            x=x,
            hyperedge_index=hyperedge_index,
            hyperedge_weights=hyperedge_weights,
            hyperedge_attr=hyperedge_attr,
            num_nodes=num_nodes,
            num_hyperedges=num_hyperedges,
            task=task,
        )

    @classmethod
    def __collect_attr_keys(cls, attr_keys: list[dict[str, Any]]) -> list[str]:
        """
        Collect unique numeric attribute keys from a list of attribute dictionaries.

        Args:
            attr_keys: List of attribute dictionaries.

        Returns:
            attr_keys: List of unique numeric attribute keys.
        """
        unique_keys = []
        for attrs in attr_keys:
            for key, value in attrs.items():
                if key not in unique_keys and isinstance(value, (int, float)):
                    unique_keys.append(key)

        return unique_keys

    @classmethod
    def __process_hyperedge_attr(
        cls,
        hypergraph: HIFHypergraph,
        hyperedge_id_to_idx: dict[Any, int],
        num_hyperedges: int,
    ) -> Tensor | None:
        """
        Build the hyperedge attribute matrix from HIF hyperedge attributes.

        Args:
            hypergraph: HIF hypergraph to process.
            hyperedge_id_to_idx: Mapping from HIF hyperedge IDs to contiguous indices.
            num_hyperedges: Number of hyperedges in the processed data.

        Returns:
            hyperedge_attr: Hyperedge attribute tensor, or ``None`` when no attributes exist.
        """
        hyperedge_attr = None  # shape [num_hyperedges, num_hyperedge_attributes]
        has_hyperedges = hypergraph.hyperedges is not None and len(hypergraph.hyperedges) > 0
        has_any_hyperedge_attrs = has_hyperedges and any(
            "attrs" in edge for edge in hypergraph.hyperedges
        )

        if has_any_hyperedge_attrs:
            hyperedge_id_to_attrs: dict[Any, dict[str, Any]] = {
                e.get("edge"): e.get("attrs", {}) for e in hypergraph.hyperedges
            }

            hyperedge_attr_keys = cls.__collect_attr_keys(list(hyperedge_id_to_attrs.values()))

            # Build attributes in exact order of hyperedge_set indices (0 to num_hyperedges - 1)
            hyperedge_idx_to_id = {idx: id for id, idx in hyperedge_id_to_idx.items()}

            attrs = []
            for hyperedge_idx in range(num_hyperedges):
                hyperedge_id = hyperedge_idx_to_id[hyperedge_idx]

                transformed_attrs = cls.transform_attrs(
                    # If it's a real hyperedge, get its attrs; if self-loop, get empty dict
                    attrs=hyperedge_id_to_attrs.get(hyperedge_id, {}),
                    attr_keys=hyperedge_attr_keys,
                )
                attrs.append(transformed_attrs)

            hyperedge_attr = torch.stack(attrs)

        return hyperedge_attr

    @classmethod
    def __process_x(cls, hypergraph: HIFHypergraph, num_nodes: int) -> Tensor:
        """
        Build the node feature matrix from HIF node attributes.

        Args:
            hypergraph: HIF hypergraph to process.
            num_nodes: Number of nodes in the processed data.

        Returns:
            x: Node feature matrix.
        """
        # Collect all attribute keys to have tensors of same size
        node_attr_keys = cls.__collect_attr_keys(
            [node.get("attrs", {}) for node in hypergraph.nodes]
        )

        if node_attr_keys:
            x = torch.stack(
                [
                    cls.transform_attrs(node.get("attrs", {}), attr_keys=node_attr_keys)
                    for node in hypergraph.nodes
                ]
            )
        else:
            # Fallback to ones if no node features, 1 is better as it can help during
            # training (e.g., avoid zero multiplication), especially in first epochs
            x = torch.ones((num_nodes, 1), dtype=torch.float)

        return x  # shape [num_nodes, num_node_features]

    @classmethod
    def __process_hyperedge_weights(
        cls,
        hypergraph: HIFHypergraph,
        hyperedge_id_to_idx: dict[Any, int],
        num_hyperedges: int,
    ) -> Tensor | None:
        """
        Build hyperedge weights from HIF hyperedge attributes.

        Args:
            hypergraph: HIF hypergraph to process.
            hyperedge_id_to_idx: Mapping from HIF hyperedge IDs to contiguous indices.
            num_hyperedges: Number of hyperedges in the processed data.

        Returns:
            hyperedge_weights: Hyperedge weight tensor, or ``None`` when no edge attributes exist.
        """
        has_hyperedges = hypergraph.hyperedges is not None and len(hypergraph.hyperedges) > 0
        has_any_hyperedge_attrs = has_hyperedges and any(
            "attrs" in edge for edge in hypergraph.hyperedges
        )

        # Keep old behavior for fixtures where edges have no attrs at all.
        if not has_any_hyperedge_attrs:
            return None

        # Map real edge id -> attrs (self-loops are absent and will default to 1.0)
        hyperedge_id_to_attrs: dict[Any, dict[str, Any]] = {
            e.get("edge"): e.get("attrs", {}) for e in hypergraph.hyperedges
        }

        # Build in exact hyperedge index order, defaulting missing weights to 1.0.
        hyperedge_idx_to_id = {idx: edge_id for edge_id, idx in hyperedge_id_to_idx.items()}
        weights = []
        for hyperedge_idx in range(num_hyperedges):
            edge_id = hyperedge_idx_to_id[hyperedge_idx]
            edge_attrs = hyperedge_id_to_attrs.get(edge_id, {})
            weights.append(float(edge_attrs.get("weight", 1.0)))

        return torch.tensor(weights, dtype=torch.float)  # shape [num_hyperedges,]

transform_attrs(attrs, attr_keys=None) staticmethod

Extract and encode numeric attributes to tensor.

Non-numeric attributes are discarded. Missing attributes are filled with 0.0.

Parameters:

Name Type Description Default
attrs dict[str, Any]

Dictionary of attributes

required
attr_keys list[str] | None

Optional list of attribute keys to encode. If provided, ensures consistent ordering and fill missing with 0.0. Defaults to None.

None

Returns:

Name Type Description
attrs Tensor

Tensor of numeric attribute values

Source code in hypertorch/data/hif.py
@staticmethod
def transform_attrs(
    attrs: dict[str, Any],
    attr_keys: list[str] | None = None,
) -> Tensor:
    """
    Extract and encode numeric attributes to tensor.

    Non-numeric attributes are discarded. Missing attributes are filled with ``0.0``.

    Args:
        attrs: Dictionary of attributes
        attr_keys: Optional list of attribute keys to encode. If provided,
            ensures consistent ordering and fill missing with ``0.0``.
            Defaults to ``None``.

    Returns:
        attrs: Tensor of numeric attribute values
    """
    numeric_attrs = {
        key: value
        for key, value in attrs.items()
        if isinstance(value, (int, float)) and not isinstance(value, bool)
    }

    if attr_keys is not None:
        values = [float(numeric_attrs.get(key, 0.0)) for key in attr_keys]
        return torch.tensor(values, dtype=torch.float)

    if not numeric_attrs:
        return torch.tensor([], dtype=torch.float)

    values = [float(value) for value in numeric_attrs.values()]
    return torch.tensor(values, dtype=torch.float)

process_hypergraph(hypergraph, task=TaskEnum.HYPERLINK_PREDICTION) classmethod

Process the loaded hypergraph into HData format, mapping HIF structure to tensors.

Returns:

Name Type Description
hdata HData

The processed hypergraph data.

Raises:

Type Description
ValueError

If HIF node IDs are not unique or an incidence references an undeclared node ID.

Source code in hypertorch/data/hif.py
@classmethod
def process_hypergraph(
    cls,
    hypergraph: HIFHypergraph,
    task: Task = TaskEnum.HYPERLINK_PREDICTION,
) -> HData:
    """
    Process the loaded hypergraph into `HData` format, mapping HIF structure to tensors.

    Returns:
        hdata: The processed hypergraph data.

    Raises:
        ValueError: If HIF node IDs are not unique or an incidence references an
            undeclared node ID.
    """
    num_nodes = len(hypergraph.nodes)
    x = cls.__process_x(hypergraph, num_nodes)

    # Remap node IDs to 0-based contiguous IDs (using indices) matching the x tensor order
    node_id_to_idx = {node.get("node"): idx for idx, node in enumerate(hypergraph.nodes)}
    if len(node_id_to_idx) != num_nodes:
        raise ValueError("HIF node IDs must be unique.")

    # Initialize edge_set only with edges that have incidences, so that
    # we avoid inflating edge count due to isolated nodes/missing incidences
    hyperedge_id_to_idx: dict[Any, int] = {}

    node_ids = []
    hyperedge_ids = []
    nodes_with_incidences = set()
    for incidence in hypergraph.incidences:
        node_id = incidence.get("node", 0)
        hyperedge_id = incidence.get("edge", 0)
        if node_id not in node_id_to_idx:
            raise ValueError(
                f"Incidence references unknown node id {node_id!r}; "
                "all incidence nodes must be declared in the HIF nodes list."
            )

        if hyperedge_id not in hyperedge_id_to_idx:
            # Hyperedges start from 0 and are assigned IDs in the order they are
            # first encountered in incidences
            hyperedge_id_to_idx[hyperedge_id] = len(hyperedge_id_to_idx)

        node_ids.append(node_id_to_idx[node_id])
        hyperedge_ids.append(hyperedge_id_to_idx[hyperedge_id])
        nodes_with_incidences.add(node_id_to_idx[node_id])

    # Handle isolated nodes by assigning them to a new unique hyperedge (self-loop)
    for node_idx in range(num_nodes):
        if node_idx not in nodes_with_incidences:
            new_hyperedge_id = len(hyperedge_id_to_idx)
            # Unique dummy key to reserve the index in hyperedge_set
            hyperedge_id_to_idx[f"__self_loop_{node_idx}__"] = new_hyperedge_id
            node_ids.append(node_idx)
            hyperedge_ids.append(new_hyperedge_id)

    num_hyperedges = len(hyperedge_id_to_idx)
    hyperedge_attr = cls.__process_hyperedge_attr(
        hypergraph=hypergraph,
        hyperedge_id_to_idx=hyperedge_id_to_idx,
        num_hyperedges=num_hyperedges,
    )

    hyperedge_weights = cls.__process_hyperedge_weights(
        hypergraph=hypergraph,
        hyperedge_id_to_idx=hyperedge_id_to_idx,
        num_hyperedges=num_hyperedges,
    )

    hyperedge_index = torch.tensor([node_ids, hyperedge_ids], dtype=torch.long)

    return HData(
        x=x,
        hyperedge_index=hyperedge_index,
        hyperedge_weights=hyperedge_weights,
        hyperedge_attr=hyperedge_attr,
        num_nodes=num_nodes,
        num_hyperedges=num_hyperedges,
        task=task,
    )

__collect_attr_keys(attr_keys) classmethod

Collect unique numeric attribute keys from a list of attribute dictionaries.

Parameters:

Name Type Description Default
attr_keys list[dict[str, Any]]

List of attribute dictionaries.

required

Returns:

Name Type Description
attr_keys list[str]

List of unique numeric attribute keys.

Source code in hypertorch/data/hif.py
@classmethod
def __collect_attr_keys(cls, attr_keys: list[dict[str, Any]]) -> list[str]:
    """
    Collect unique numeric attribute keys from a list of attribute dictionaries.

    Args:
        attr_keys: List of attribute dictionaries.

    Returns:
        attr_keys: List of unique numeric attribute keys.
    """
    unique_keys = []
    for attrs in attr_keys:
        for key, value in attrs.items():
            if key not in unique_keys and isinstance(value, (int, float)):
                unique_keys.append(key)

    return unique_keys

__process_hyperedge_attr(hypergraph, hyperedge_id_to_idx, num_hyperedges) classmethod

Build the hyperedge attribute matrix from HIF hyperedge attributes.

Parameters:

Name Type Description Default
hypergraph HIFHypergraph

HIF hypergraph to process.

required
hyperedge_id_to_idx dict[Any, int]

Mapping from HIF hyperedge IDs to contiguous indices.

required
num_hyperedges int

Number of hyperedges in the processed data.

required

Returns:

Name Type Description
hyperedge_attr Tensor | None

Hyperedge attribute tensor, or None when no attributes exist.

Source code in hypertorch/data/hif.py
@classmethod
def __process_hyperedge_attr(
    cls,
    hypergraph: HIFHypergraph,
    hyperedge_id_to_idx: dict[Any, int],
    num_hyperedges: int,
) -> Tensor | None:
    """
    Build the hyperedge attribute matrix from HIF hyperedge attributes.

    Args:
        hypergraph: HIF hypergraph to process.
        hyperedge_id_to_idx: Mapping from HIF hyperedge IDs to contiguous indices.
        num_hyperedges: Number of hyperedges in the processed data.

    Returns:
        hyperedge_attr: Hyperedge attribute tensor, or ``None`` when no attributes exist.
    """
    hyperedge_attr = None  # shape [num_hyperedges, num_hyperedge_attributes]
    has_hyperedges = hypergraph.hyperedges is not None and len(hypergraph.hyperedges) > 0
    has_any_hyperedge_attrs = has_hyperedges and any(
        "attrs" in edge for edge in hypergraph.hyperedges
    )

    if has_any_hyperedge_attrs:
        hyperedge_id_to_attrs: dict[Any, dict[str, Any]] = {
            e.get("edge"): e.get("attrs", {}) for e in hypergraph.hyperedges
        }

        hyperedge_attr_keys = cls.__collect_attr_keys(list(hyperedge_id_to_attrs.values()))

        # Build attributes in exact order of hyperedge_set indices (0 to num_hyperedges - 1)
        hyperedge_idx_to_id = {idx: id for id, idx in hyperedge_id_to_idx.items()}

        attrs = []
        for hyperedge_idx in range(num_hyperedges):
            hyperedge_id = hyperedge_idx_to_id[hyperedge_idx]

            transformed_attrs = cls.transform_attrs(
                # If it's a real hyperedge, get its attrs; if self-loop, get empty dict
                attrs=hyperedge_id_to_attrs.get(hyperedge_id, {}),
                attr_keys=hyperedge_attr_keys,
            )
            attrs.append(transformed_attrs)

        hyperedge_attr = torch.stack(attrs)

    return hyperedge_attr

__process_x(hypergraph, num_nodes) classmethod

Build the node feature matrix from HIF node attributes.

Parameters:

Name Type Description Default
hypergraph HIFHypergraph

HIF hypergraph to process.

required
num_nodes int

Number of nodes in the processed data.

required

Returns:

Name Type Description
x Tensor

Node feature matrix.

Source code in hypertorch/data/hif.py
@classmethod
def __process_x(cls, hypergraph: HIFHypergraph, num_nodes: int) -> Tensor:
    """
    Build the node feature matrix from HIF node attributes.

    Args:
        hypergraph: HIF hypergraph to process.
        num_nodes: Number of nodes in the processed data.

    Returns:
        x: Node feature matrix.
    """
    # Collect all attribute keys to have tensors of same size
    node_attr_keys = cls.__collect_attr_keys(
        [node.get("attrs", {}) for node in hypergraph.nodes]
    )

    if node_attr_keys:
        x = torch.stack(
            [
                cls.transform_attrs(node.get("attrs", {}), attr_keys=node_attr_keys)
                for node in hypergraph.nodes
            ]
        )
    else:
        # Fallback to ones if no node features, 1 is better as it can help during
        # training (e.g., avoid zero multiplication), especially in first epochs
        x = torch.ones((num_nodes, 1), dtype=torch.float)

    return x  # shape [num_nodes, num_node_features]

__process_hyperedge_weights(hypergraph, hyperedge_id_to_idx, num_hyperedges) classmethod

Build hyperedge weights from HIF hyperedge attributes.

Parameters:

Name Type Description Default
hypergraph HIFHypergraph

HIF hypergraph to process.

required
hyperedge_id_to_idx dict[Any, int]

Mapping from HIF hyperedge IDs to contiguous indices.

required
num_hyperedges int

Number of hyperedges in the processed data.

required

Returns:

Name Type Description
hyperedge_weights Tensor | None

Hyperedge weight tensor, or None when no edge attributes exist.

Source code in hypertorch/data/hif.py
@classmethod
def __process_hyperedge_weights(
    cls,
    hypergraph: HIFHypergraph,
    hyperedge_id_to_idx: dict[Any, int],
    num_hyperedges: int,
) -> Tensor | None:
    """
    Build hyperedge weights from HIF hyperedge attributes.

    Args:
        hypergraph: HIF hypergraph to process.
        hyperedge_id_to_idx: Mapping from HIF hyperedge IDs to contiguous indices.
        num_hyperedges: Number of hyperedges in the processed data.

    Returns:
        hyperedge_weights: Hyperedge weight tensor, or ``None`` when no edge attributes exist.
    """
    has_hyperedges = hypergraph.hyperedges is not None and len(hypergraph.hyperedges) > 0
    has_any_hyperedge_attrs = has_hyperedges and any(
        "attrs" in edge for edge in hypergraph.hyperedges
    )

    # Keep old behavior for fixtures where edges have no attrs at all.
    if not has_any_hyperedge_attrs:
        return None

    # Map real edge id -> attrs (self-loops are absent and will default to 1.0)
    hyperedge_id_to_attrs: dict[Any, dict[str, Any]] = {
        e.get("edge"): e.get("attrs", {}) for e in hypergraph.hyperedges
    }

    # Build in exact hyperedge index order, defaulting missing weights to 1.0.
    hyperedge_idx_to_id = {idx: edge_id for edge_id, idx in hyperedge_id_to_idx.items()}
    weights = []
    for hyperedge_idx in range(num_hyperedges):
        edge_id = hyperedge_idx_to_id[hyperedge_idx]
        edge_attrs = hyperedge_id_to_attrs.get(edge_id, {})
        weights.append(float(edge_attrs.get("weight", 1.0)))

    return torch.tensor(weights, dtype=torch.float)  # shape [num_hyperedges,]

AlgebraDataset

Bases: _PreloadedDataset

Preloaded algebra dataset.

Source code in hypertorch/data/supported_datasets.py
class AlgebraDataset(_PreloadedDataset):
    """
    Preloaded algebra dataset.
    """

    DATASET_NAME: ClassVar[str] = "algebra"
    HF_SHA: ClassVar[str | None] = "2bb641461e00c103fb5ef4fe6a30aad42500fc21"

AmazonDataset

Bases: _PreloadedDataset

Preloaded Amazon reviews dataset.

Source code in hypertorch/data/supported_datasets.py
class AmazonDataset(_PreloadedDataset):
    """
    Preloaded Amazon reviews dataset.
    """

    DATASET_NAME: ClassVar[str] = "amazon"
    HF_SHA: ClassVar[str | None] = "614f75d1847d233ee06da0cc3ee10f51220b8243"

ContactHighSchoolDataset

Bases: _PreloadedDataset

Preloaded high-school contact network dataset.

Source code in hypertorch/data/supported_datasets.py
class ContactHighSchoolDataset(_PreloadedDataset):
    """
    Preloaded high-school contact network dataset.
    """

    DATASET_NAME: ClassVar[str] = "contact-high-school"
    HF_SHA: ClassVar[str | None] = "b991fde34631a357961a244a5c4d734cf3093199"

ContactPrimarySchoolDataset

Bases: _PreloadedDataset

Preloaded primary-school contact network dataset.

Source code in hypertorch/data/supported_datasets.py
class ContactPrimarySchoolDataset(_PreloadedDataset):
    """
    Preloaded primary-school contact network dataset.
    """

    DATASET_NAME: ClassVar[str] = "contact-primary-school"
    HF_SHA: ClassVar[str | None] = "f6f5453777d1fc62f6305b17d131ec1e32cdbe66"

CoraDataset

Bases: _PreloadedDataset

Preloaded Cora citation dataset.

Source code in hypertorch/data/supported_datasets.py
class CoraDataset(_PreloadedDataset):
    """
    Preloaded Cora citation dataset.
    """

    DATASET_NAME: ClassVar[str] = "cora"
    HF_SHA: ClassVar[str | None] = "dc0f94770bd4f4f7174fa8d02318435330812b42"

CourseraDataset

Bases: _PreloadedDataset

Preloaded Coursera dataset.

Source code in hypertorch/data/supported_datasets.py
class CourseraDataset(_PreloadedDataset):
    """
    Preloaded Coursera dataset.
    """

    DATASET_NAME: ClassVar[str] = "coursera"
    HF_SHA: ClassVar[str | None] = "e68679a01af61c43292575839e451eb0bbeee202"

DBLPDataset

Bases: _PreloadedDataset

Preloaded DBLP co-authorship dataset.

Source code in hypertorch/data/supported_datasets.py
class DBLPDataset(_PreloadedDataset):
    """
    Preloaded DBLP co-authorship dataset.
    """

    DATASET_NAME: ClassVar[str] = "dblp"
    HF_SHA: ClassVar[str | None] = "151c360ed77042abebb9709fd3d738763d5c5044"

EmailEnronDataset

Bases: _PreloadedDataset

Preloaded Enron email dataset.

Source code in hypertorch/data/supported_datasets.py
class EmailEnronDataset(_PreloadedDataset):
    """
    Preloaded Enron email dataset.
    """

    DATASET_NAME: ClassVar[str] = "email-Enron"
    HF_SHA: ClassVar[str | None] = "05247a5441a6a337cdccee24c0060255815905be"

EmailW3CDataset

Bases: _PreloadedDataset

Preloaded W3C email dataset.

Source code in hypertorch/data/supported_datasets.py
class EmailW3CDataset(_PreloadedDataset):
    """
    Preloaded W3C email dataset.
    """

    DATASET_NAME: ClassVar[str] = "email-W3C"
    HF_SHA: ClassVar[str | None] = "18b8c795504388c1d075ffcea7eada281ec5e416"

GeometryDataset

Bases: _PreloadedDataset

Preloaded geometry dataset.

Source code in hypertorch/data/supported_datasets.py
class GeometryDataset(_PreloadedDataset):
    """
    Preloaded geometry dataset.
    """

    DATASET_NAME: ClassVar[str] = "geometry"
    HF_SHA: ClassVar[str | None] = "49a8647d6ff7361485c953949010155b0b522a12"

GOTDataset

Bases: _PreloadedDataset

Preloaded Game of Thrones dataset.

Source code in hypertorch/data/supported_datasets.py
class GOTDataset(_PreloadedDataset):
    """
    Preloaded Game of Thrones dataset.
    """

    DATASET_NAME: ClassVar[str] = "got"
    HF_SHA: ClassVar[str | None] = "2efb505e5d82457f6e5ba21820c8d8f2298f0ece"

IMDBDataset

Bases: _PreloadedDataset

Preloaded IMDB dataset.

Source code in hypertorch/data/supported_datasets.py
class IMDBDataset(_PreloadedDataset):
    """
    Preloaded IMDB dataset.
    """

    DATASET_NAME: ClassVar[str] = "imdb"
    HF_SHA: ClassVar[str | None] = "c3a583313d1611b292933d77e725b11be2c39a05"

MusicBluesReviewsDataset

Bases: _PreloadedDataset

Preloaded music blues reviews dataset.

Source code in hypertorch/data/supported_datasets.py
class MusicBluesReviewsDataset(_PreloadedDataset):
    """
    Preloaded music blues reviews dataset.
    """

    DATASET_NAME: ClassVar[str] = "music-blues-reviews"
    HF_SHA: ClassVar[str | None] = "7d218b727097ed007e7f368ab91c064b3eeff184"

NBADataset

Bases: _PreloadedDataset

Preloaded NBA dataset.

Source code in hypertorch/data/supported_datasets.py
class NBADataset(_PreloadedDataset):
    """
    Preloaded NBA dataset.
    """

    DATASET_NAME: ClassVar[str] = "nba"
    HF_SHA: ClassVar[str | None] = "5b3b1c7e425bc407bc0843f443cdf889b51e1ca7"

NDCClassesDataset

Bases: _PreloadedDataset

Preloaded NDC classes dataset.

Source code in hypertorch/data/supported_datasets.py
class NDCClassesDataset(_PreloadedDataset):
    """
    Preloaded NDC classes dataset.
    """

    DATASET_NAME: ClassVar[str] = "NDC-classes"
    HF_SHA: ClassVar[str | None] = "c9bb31897646fb3f964ee4affe126f9885954d92"

NDCSubstancesDataset

Bases: _PreloadedDataset

Preloaded NDC substances dataset.

Source code in hypertorch/data/supported_datasets.py
class NDCSubstancesDataset(_PreloadedDataset):
    """
    Preloaded NDC substances dataset.
    """

    DATASET_NAME: ClassVar[str] = "NDC-substances"
    HF_SHA: ClassVar[str | None] = "bbdde0839ca5913a2535e6fe3ce397b990803af9"

PatentDataset

Bases: _PreloadedDataset

Preloaded patent dataset.

Source code in hypertorch/data/supported_datasets.py
class PatentDataset(_PreloadedDataset):
    """
    Preloaded patent dataset.
    """

    DATASET_NAME: ClassVar[str] = "patent"
    HF_SHA: ClassVar[str | None] = "608b4fab97d17adbc01b0b4636b060a550231307"

PubmedDataset

Bases: _PreloadedDataset

Preloaded PubMed dataset.

Source code in hypertorch/data/supported_datasets.py
class PubmedDataset(_PreloadedDataset):
    """
    Preloaded PubMed dataset.
    """

    DATASET_NAME: ClassVar[str] = "pubmed"
    HF_SHA: ClassVar[str | None] = "b8f846a3c812b3b23f10bd69f65f739983f6a390"

RestaurantReviewsDataset

Bases: _PreloadedDataset

Preloaded restaurant reviews dataset.

Source code in hypertorch/data/supported_datasets.py
class RestaurantReviewsDataset(_PreloadedDataset):
    """
    Preloaded restaurant reviews dataset.
    """

    DATASET_NAME: ClassVar[str] = "restaurant-reviews"
    HF_SHA: ClassVar[str | None] = "668a90391fcb968c786da7bc9e7bbc55e2832066"

ThreadsAskUbuntuDataset

Bases: _PreloadedDataset

Preloaded Ask Ubuntu thread dataset.

Source code in hypertorch/data/supported_datasets.py
class ThreadsAskUbuntuDataset(_PreloadedDataset):
    """
    Preloaded Ask Ubuntu thread dataset.
    """

    DATASET_NAME: ClassVar[str] = "threads-ask-ubuntu"
    HF_SHA: ClassVar[str | None] = "704c54c7f21b4e313ab6bb50bcd30f58ade469b6"

ThreadsMathsxDataset

Bases: _PreloadedDataset

Preloaded Math StackExchange thread dataset.

Source code in hypertorch/data/supported_datasets.py
class ThreadsMathsxDataset(_PreloadedDataset):
    """
    Preloaded Math StackExchange thread dataset.
    """

    DATASET_NAME: ClassVar[str] = "threads-math-sx"
    HF_SHA: ClassVar[str | None] = "b024111c16fdb266e159a4c647ff1a31ec40db5b"

TwitterDataset

Bases: _PreloadedDataset

Preloaded Twitter dataset.

Source code in hypertorch/data/supported_datasets.py
class TwitterDataset(_PreloadedDataset):
    """
    Preloaded Twitter dataset.
    """

    DATASET_NAME: ClassVar[str] = "twitter"
    HF_SHA: ClassVar[str | None] = "d93c55af8e04cf70d65ed0059325009a21699a25"

VegasBarsReviewsDataset

Bases: _PreloadedDataset

Preloaded Vegas bars reviews dataset.

Source code in hypertorch/data/supported_datasets.py
class VegasBarsReviewsDataset(_PreloadedDataset):
    """
    Preloaded Vegas bars reviews dataset.
    """

    DATASET_NAME: ClassVar[str] = "vegas-bars-reviews"
    HF_SHA: ClassVar[str | None] = "4f1e4e4c87957679efc38c05129a694d315a8c9b"

DataLoader

Bases: DataLoader

DataLoader combines a dataset and a sampler, and provides an iterable over the given dataset. It extends torch.utils.data.DataLoader.

Source code in hypertorch/data/loader.py
class DataLoader(TorchDataLoader):
    """
    DataLoader combines a dataset and a sampler, and provides an iterable
    over the given dataset. It extends ``torch.utils.data.DataLoader``.
    """

    def __init__(
        self,
        dataset: Dataset,
        batch_size: int = 1,
        shuffle: bool | None = False,
        sample_full_hypergraph: bool = False,
        drop_last: bool = False,
        num_workers: int = 0,
        persistent_workers: bool = False,
        collate_fn: Callable[[list[HData]], HData] | None = None,
        generator: Generator | None = None,
        **kwargs: Any,
    ) -> None:
        """
        Initialize the data loader.

        Args:
            dataset: Dataset that provides sampled `HData` objects.
            batch_size: Number of samples per batch. Ignored when
                ``sample_full_hypergraph`` is ``True`` because the full dataset
                is loaded as one batch.
            shuffle: Whether to reshuffle sample indices at every epoch.
            sample_full_hypergraph: Whether each collated batch should ignore the sampled
                mini-batch subgraphs and return a clone of the dataset's full `HData`.
                If ``False``, batches contain only the sampled local subgraph.
                For dense transductive splits, this preserves the full graph as model
                context while `target_node_mask` or `target_hyperedge_mask` identifies the
                supervised nodes or hyperedges for the split.
                Defaults to ``False``.
            drop_last: Whether to drop the final incomplete batch when the dataset size
                is not divisible by the batch size. If ``True``, drop the last incomplete batch.
                If ``False``, the last batch will be kept and will be smaller.
                Defaults to ``False``.
            num_workers: Optional number of subprocesses to use for data loading.
                For instance, ``0`` means loading happens in the main process. Defaults to ``0``.
            persistent_workers: Whether worker processes should stay alive after a dataset has
                been consumed once. If ``True``, the data loader will not shut down
                the worker processes after a dataset has been consumed once. This allows to
                maintain the workers `Dataset` instances alive. Defaults to ``False``.
            collate_fn: Optional custom collate function. When ``None``, uses
                the default `collate` method.
            generator: Optional random generator used by the underlying Torch data loader.
            kwargs:
                sampler: Defines the strategy to draw samples from the dataset.
                    Can be any ``Iterable`` with ``__len__`` implemented.
                    Mutually exclusive with ``shuffle``. Defaults to ``None``.
                batch_sampler: Defines the strategy to draw batches of indices. Mutually exclusive
                    with ``batch_size``, ``shuffle``, ``sampler``, and ``drop_last``.
                    Defaults to ``None``.
                pin_memory: Whether to copy tensors into pinned memory before returning them.
                    If ``True``, the data loader will copy tensors into device/CUDA pinned memory
                    before returning them. If your data elements are a custom type, or `collate_fn`
                    returns a batch that is a custom type, look at ``torch.utils.data.DataLoader``
                    for examples. Defaults to ``False``.
                timeout: Timeout, in seconds, for collecting a batch from worker processes.
                    Should always be non-negative and defaults to ``0``.
                worker_init_fn: If not ``None``, this will be called on each worker subprocess
                    with the worker id (an int in ``[0, num_workers - 1]``) as input, after seeding
                    and before data loading. Defaults to ``None``.
                multiprocessing_context: Multiprocessing context or context name used to create
                    worker processes.
                prefetch_factor: Number of batches loaded in advance by each worker.
                    For example, ``2`` means there will be a total of ``2 * num_workers`` batches
                    prefetched across all workers. The default value depends on the set value
                    for num_workers. If ``num_workers=0``, defaults to ``None``.
                    If ``num_workers > 0``, defaults to ``2``.
                in_order: If ``True``, batches are returned in first-in, first-out order when
                    ``num_workers > 0``. Defaults to ``True``.
        """
        self.__sample_full_hypergraph: bool = sample_full_hypergraph

        super().__init__(
            dataset=dataset,
            batch_size=len(dataset) if self.__sample_full_hypergraph else batch_size,
            shuffle=shuffle,
            collate_fn=self.collate if collate_fn is None else collate_fn,
            generator=generator,
            num_workers=num_workers,
            persistent_workers=persistent_workers,
            drop_last=drop_last,
            **kwargs,
        )

        self.__cached_dataset_hdata: HData = dataset.hdata

    def collate(self, batch: list[HData]) -> HData:
        """
        Collates a list of `HData` objects into a single batched `HData` object.

        This function combines multiple separate samples into a single batched representation
        suitable for mini-batch training.

        Handles:
            - Concatenating node features from all samples.
            - Concatenating and offsetting hyperedges from all samples.
            - Concatenating hyperedge attributes from all samples, if present.
            - Concatenating hyperedge weights from all samples, if present.

        Examples:
            Given ``batch = [HData_0, HData_1]``:

            For node features:

            >>> HData_0.x.shape  # (3, 64) — 3 nodes with 64 features
            >>> HData_1.x.shape  # (2, 64) — 2 nodes with 64 features
            >>> x.shape  # (5, 64) — all 5 nodes concatenated

            For hyperedge index:

            - ``HData_0`` (3 nodes, 2 hyperedges):

            >>> hyperedge_index = [[0, 1, 1, 2],  # Nodes 0, 1, 1, 2
            ...                    [0, 0, 1, 1]]  # HE 0 contains {0,1}, HE 1 contains {1,2}

            - ``HData_1`` (2 nodes, 1 hyperedge):

            >>> hyperedge_index = [[0, 1],  # Nodes 0, 1
            ...                    [0, 0]]  # Hyperedge 0 contains {0,1}

            Batched result:

            >>> hyperedge_index = [[0, 1, 1, 2, 3, 4],  # Node indices: original then offset by 3
            ...                    [0, 0, 1, 1, 2, 2]]  # Hyperedge IDs: original then offset by 2

        Args:
            batch: List of `HData` objects to collate.

        Returns:
            hdata: A single `HData` object containing the collated data.
        """
        if self.__sample_full_hypergraph:
            return self.__cached_dataset_hdata.clone().to(batch[0].device)

        collated_hyperedge_index = torch.cat([data.hyperedge_index for data in batch], dim=1)
        hyperedge_index_wrapper = HyperedgeIndex(collated_hyperedge_index).remove_duplicate_edges()

        hyperedge_ids = hyperedge_index_wrapper.hyperedge_ids
        node_ids = hyperedge_index_wrapper.node_ids

        collated_x = self.__cached_dataset_hdata.x[node_ids]
        collated_global_node_ids = self.__cached_dataset_hdata.global_node_ids[node_ids]

        (
            collated_y,
            collated_target_node_mask,
            collated_target_hyperedge_mask,
        ) = self.__collate_y_and_target_masks_for_task(
            batch=batch,
            hyperedge_index_wrapper=hyperedge_index_wrapper,
        )

        collated_hyperedge_attr = (
            self.__cached_dataset_hdata.hyperedge_attr[hyperedge_ids]
            if self.__cached_dataset_hdata.hyperedge_attr is not None
            else None
        )

        collated_hyperedge_weights = (
            self.__cached_dataset_hdata.hyperedge_weights[hyperedge_ids]
            if self.__cached_dataset_hdata.hyperedge_weights is not None
            else None
        )

        collated_hyperedge_index = hyperedge_index_wrapper.to_0based().item

        collated_hdata = HData(
            x=collated_x,
            hyperedge_index=collated_hyperedge_index,
            hyperedge_weights=collated_hyperedge_weights,
            hyperedge_attr=collated_hyperedge_attr,
            num_nodes=hyperedge_index_wrapper.num_nodes,
            num_hyperedges=hyperedge_index_wrapper.num_hyperedges,
            global_node_ids=collated_global_node_ids,
            target_node_mask=collated_target_node_mask,
            target_hyperedge_mask=collated_target_hyperedge_mask,
            y=collated_y,
            task=self.__cached_dataset_hdata.task,
        )

        return collated_hdata.to(batch[0].device)

    def __collate_y_and_target_masks_for_task(
        self,
        batch: list[HData],
        hyperedge_index_wrapper: HyperedgeIndex,
    ) -> tuple[Tensor, Tensor | None, Tensor | None]:
        """
        Collates the labels (y) and target masks for a batch of
        HData instances based on the task type.

        Args:
            batch: List of HData instances containing the data to collate.
            hyperedge_index_wrapper: A HyperedgeIndex wrapping the collated hyperedge index.

        Returns:
            collated_y: A tensor containing the collated labels for the batch.
            collated_target_node_mask: A tensor containing the collated target node mask
                for the batch, or ``None`` if not applicable.
            collated_target_hyperedge_mask: A tensor containing the collated target hyperedge
                mask for the batch, or ``None`` if not applicable.
        """
        node_ids = hyperedge_index_wrapper.node_ids
        hyperedge_ids = hyperedge_index_wrapper.hyperedge_ids

        if self.__cached_dataset_hdata.is_node_related_task:
            collated_y = self.__cached_dataset_hdata.y[node_ids]
            collated_target_hyperedge_mask = None

            target_node_ids_list = [
                HyperedgeIndex(hdata.hyperedge_index).node_ids[hdata.target_node_mask]
                for hdata in batch
            ]
            target_node_ids = torch.cat(target_node_ids_list, dim=0)
            collated_target_node_mask = torch.isin(node_ids, target_node_ids)
        elif self.__cached_dataset_hdata.is_hyperedge_related_task:
            collated_y = self.__cached_dataset_hdata.y[hyperedge_ids]
            collated_target_node_mask = None

            target_hyperedge_ids_list = [
                HyperedgeIndex(hdata.hyperedge_index).hyperedge_ids[hdata.target_hyperedge_mask]
                for hdata in batch
            ]
            target_hyperedge_ids = torch.cat(target_hyperedge_ids_list, dim=0)
            collated_target_hyperedge_mask = torch.isin(hyperedge_ids, target_hyperedge_ids)
        else:
            raise ValueError(
                f"Unsupported task category for task={self.__cached_dataset_hdata.task!r}."
            )

        return collated_y, collated_target_node_mask, collated_target_hyperedge_mask

__init__(dataset, batch_size=1, shuffle=False, sample_full_hypergraph=False, drop_last=False, num_workers=0, persistent_workers=False, collate_fn=None, generator=None, **kwargs)

Initialize the data loader.

Parameters:

Name Type Description Default
dataset Dataset

Dataset that provides sampled HData objects.

required
batch_size int

Number of samples per batch. Ignored when sample_full_hypergraph is True because the full dataset is loaded as one batch.

1
shuffle bool | None

Whether to reshuffle sample indices at every epoch.

False
sample_full_hypergraph bool

Whether each collated batch should ignore the sampled mini-batch subgraphs and return a clone of the dataset's full HData. If False, batches contain only the sampled local subgraph. For dense transductive splits, this preserves the full graph as model context while target_node_mask or target_hyperedge_mask identifies the supervised nodes or hyperedges for the split. Defaults to False.

False
drop_last bool

Whether to drop the final incomplete batch when the dataset size is not divisible by the batch size. If True, drop the last incomplete batch. If False, the last batch will be kept and will be smaller. Defaults to False.

False
num_workers int

Optional number of subprocesses to use for data loading. For instance, 0 means loading happens in the main process. Defaults to 0.

0
persistent_workers bool

Whether worker processes should stay alive after a dataset has been consumed once. If True, the data loader will not shut down the worker processes after a dataset has been consumed once. This allows to maintain the workers Dataset instances alive. Defaults to False.

False
collate_fn Callable[[list[HData]], HData] | None

Optional custom collate function. When None, uses the default collate method.

None
generator Generator | None

Optional random generator used by the underlying Torch data loader.

None
kwargs Any

sampler: Defines the strategy to draw samples from the dataset. Can be any Iterable with __len__ implemented. Mutually exclusive with shuffle. Defaults to None. batch_sampler: Defines the strategy to draw batches of indices. Mutually exclusive with batch_size, shuffle, sampler, and drop_last. Defaults to None. pin_memory: Whether to copy tensors into pinned memory before returning them. If True, the data loader will copy tensors into device/CUDA pinned memory before returning them. If your data elements are a custom type, or collate_fn returns a batch that is a custom type, look at torch.utils.data.DataLoader for examples. Defaults to False. timeout: Timeout, in seconds, for collecting a batch from worker processes. Should always be non-negative and defaults to 0. worker_init_fn: If not None, this will be called on each worker subprocess with the worker id (an int in [0, num_workers - 1]) as input, after seeding and before data loading. Defaults to None. multiprocessing_context: Multiprocessing context or context name used to create worker processes. prefetch_factor: Number of batches loaded in advance by each worker. For example, 2 means there will be a total of 2 * num_workers batches prefetched across all workers. The default value depends on the set value for num_workers. If num_workers=0, defaults to None. If num_workers > 0, defaults to 2. in_order: If True, batches are returned in first-in, first-out order when num_workers > 0. Defaults to True.

{}
Source code in hypertorch/data/loader.py
def __init__(
    self,
    dataset: Dataset,
    batch_size: int = 1,
    shuffle: bool | None = False,
    sample_full_hypergraph: bool = False,
    drop_last: bool = False,
    num_workers: int = 0,
    persistent_workers: bool = False,
    collate_fn: Callable[[list[HData]], HData] | None = None,
    generator: Generator | None = None,
    **kwargs: Any,
) -> None:
    """
    Initialize the data loader.

    Args:
        dataset: Dataset that provides sampled `HData` objects.
        batch_size: Number of samples per batch. Ignored when
            ``sample_full_hypergraph`` is ``True`` because the full dataset
            is loaded as one batch.
        shuffle: Whether to reshuffle sample indices at every epoch.
        sample_full_hypergraph: Whether each collated batch should ignore the sampled
            mini-batch subgraphs and return a clone of the dataset's full `HData`.
            If ``False``, batches contain only the sampled local subgraph.
            For dense transductive splits, this preserves the full graph as model
            context while `target_node_mask` or `target_hyperedge_mask` identifies the
            supervised nodes or hyperedges for the split.
            Defaults to ``False``.
        drop_last: Whether to drop the final incomplete batch when the dataset size
            is not divisible by the batch size. If ``True``, drop the last incomplete batch.
            If ``False``, the last batch will be kept and will be smaller.
            Defaults to ``False``.
        num_workers: Optional number of subprocesses to use for data loading.
            For instance, ``0`` means loading happens in the main process. Defaults to ``0``.
        persistent_workers: Whether worker processes should stay alive after a dataset has
            been consumed once. If ``True``, the data loader will not shut down
            the worker processes after a dataset has been consumed once. This allows to
            maintain the workers `Dataset` instances alive. Defaults to ``False``.
        collate_fn: Optional custom collate function. When ``None``, uses
            the default `collate` method.
        generator: Optional random generator used by the underlying Torch data loader.
        kwargs:
            sampler: Defines the strategy to draw samples from the dataset.
                Can be any ``Iterable`` with ``__len__`` implemented.
                Mutually exclusive with ``shuffle``. Defaults to ``None``.
            batch_sampler: Defines the strategy to draw batches of indices. Mutually exclusive
                with ``batch_size``, ``shuffle``, ``sampler``, and ``drop_last``.
                Defaults to ``None``.
            pin_memory: Whether to copy tensors into pinned memory before returning them.
                If ``True``, the data loader will copy tensors into device/CUDA pinned memory
                before returning them. If your data elements are a custom type, or `collate_fn`
                returns a batch that is a custom type, look at ``torch.utils.data.DataLoader``
                for examples. Defaults to ``False``.
            timeout: Timeout, in seconds, for collecting a batch from worker processes.
                Should always be non-negative and defaults to ``0``.
            worker_init_fn: If not ``None``, this will be called on each worker subprocess
                with the worker id (an int in ``[0, num_workers - 1]``) as input, after seeding
                and before data loading. Defaults to ``None``.
            multiprocessing_context: Multiprocessing context or context name used to create
                worker processes.
            prefetch_factor: Number of batches loaded in advance by each worker.
                For example, ``2`` means there will be a total of ``2 * num_workers`` batches
                prefetched across all workers. The default value depends on the set value
                for num_workers. If ``num_workers=0``, defaults to ``None``.
                If ``num_workers > 0``, defaults to ``2``.
            in_order: If ``True``, batches are returned in first-in, first-out order when
                ``num_workers > 0``. Defaults to ``True``.
    """
    self.__sample_full_hypergraph: bool = sample_full_hypergraph

    super().__init__(
        dataset=dataset,
        batch_size=len(dataset) if self.__sample_full_hypergraph else batch_size,
        shuffle=shuffle,
        collate_fn=self.collate if collate_fn is None else collate_fn,
        generator=generator,
        num_workers=num_workers,
        persistent_workers=persistent_workers,
        drop_last=drop_last,
        **kwargs,
    )

    self.__cached_dataset_hdata: HData = dataset.hdata

collate(batch)

Collates a list of HData objects into a single batched HData object.

This function combines multiple separate samples into a single batched representation suitable for mini-batch training.

Handles
  • Concatenating node features from all samples.
  • Concatenating and offsetting hyperedges from all samples.
  • Concatenating hyperedge attributes from all samples, if present.
  • Concatenating hyperedge weights from all samples, if present.

Examples:

Given batch = [HData_0, HData_1]:

For node features:

>>> HData_0.x.shape  # (3, 64) — 3 nodes with 64 features
>>> HData_1.x.shape  # (2, 64) — 2 nodes with 64 features
>>> x.shape  # (5, 64) — all 5 nodes concatenated

For hyperedge index:

  • HData_0 (3 nodes, 2 hyperedges):
>>> hyperedge_index = [[0, 1, 1, 2],  # Nodes 0, 1, 1, 2
...                    [0, 0, 1, 1]]  # HE 0 contains {0,1}, HE 1 contains {1,2}
  • HData_1 (2 nodes, 1 hyperedge):
>>> hyperedge_index = [[0, 1],  # Nodes 0, 1
...                    [0, 0]]  # Hyperedge 0 contains {0,1}

Batched result:

>>> hyperedge_index = [[0, 1, 1, 2, 3, 4],  # Node indices: original then offset by 3
...                    [0, 0, 1, 1, 2, 2]]  # Hyperedge IDs: original then offset by 2

Parameters:

Name Type Description Default
batch list[HData]

List of HData objects to collate.

required

Returns:

Name Type Description
hdata HData

A single HData object containing the collated data.

Source code in hypertorch/data/loader.py
def collate(self, batch: list[HData]) -> HData:
    """
    Collates a list of `HData` objects into a single batched `HData` object.

    This function combines multiple separate samples into a single batched representation
    suitable for mini-batch training.

    Handles:
        - Concatenating node features from all samples.
        - Concatenating and offsetting hyperedges from all samples.
        - Concatenating hyperedge attributes from all samples, if present.
        - Concatenating hyperedge weights from all samples, if present.

    Examples:
        Given ``batch = [HData_0, HData_1]``:

        For node features:

        >>> HData_0.x.shape  # (3, 64) — 3 nodes with 64 features
        >>> HData_1.x.shape  # (2, 64) — 2 nodes with 64 features
        >>> x.shape  # (5, 64) — all 5 nodes concatenated

        For hyperedge index:

        - ``HData_0`` (3 nodes, 2 hyperedges):

        >>> hyperedge_index = [[0, 1, 1, 2],  # Nodes 0, 1, 1, 2
        ...                    [0, 0, 1, 1]]  # HE 0 contains {0,1}, HE 1 contains {1,2}

        - ``HData_1`` (2 nodes, 1 hyperedge):

        >>> hyperedge_index = [[0, 1],  # Nodes 0, 1
        ...                    [0, 0]]  # Hyperedge 0 contains {0,1}

        Batched result:

        >>> hyperedge_index = [[0, 1, 1, 2, 3, 4],  # Node indices: original then offset by 3
        ...                    [0, 0, 1, 1, 2, 2]]  # Hyperedge IDs: original then offset by 2

    Args:
        batch: List of `HData` objects to collate.

    Returns:
        hdata: A single `HData` object containing the collated data.
    """
    if self.__sample_full_hypergraph:
        return self.__cached_dataset_hdata.clone().to(batch[0].device)

    collated_hyperedge_index = torch.cat([data.hyperedge_index for data in batch], dim=1)
    hyperedge_index_wrapper = HyperedgeIndex(collated_hyperedge_index).remove_duplicate_edges()

    hyperedge_ids = hyperedge_index_wrapper.hyperedge_ids
    node_ids = hyperedge_index_wrapper.node_ids

    collated_x = self.__cached_dataset_hdata.x[node_ids]
    collated_global_node_ids = self.__cached_dataset_hdata.global_node_ids[node_ids]

    (
        collated_y,
        collated_target_node_mask,
        collated_target_hyperedge_mask,
    ) = self.__collate_y_and_target_masks_for_task(
        batch=batch,
        hyperedge_index_wrapper=hyperedge_index_wrapper,
    )

    collated_hyperedge_attr = (
        self.__cached_dataset_hdata.hyperedge_attr[hyperedge_ids]
        if self.__cached_dataset_hdata.hyperedge_attr is not None
        else None
    )

    collated_hyperedge_weights = (
        self.__cached_dataset_hdata.hyperedge_weights[hyperedge_ids]
        if self.__cached_dataset_hdata.hyperedge_weights is not None
        else None
    )

    collated_hyperedge_index = hyperedge_index_wrapper.to_0based().item

    collated_hdata = HData(
        x=collated_x,
        hyperedge_index=collated_hyperedge_index,
        hyperedge_weights=collated_hyperedge_weights,
        hyperedge_attr=collated_hyperedge_attr,
        num_nodes=hyperedge_index_wrapper.num_nodes,
        num_hyperedges=hyperedge_index_wrapper.num_hyperedges,
        global_node_ids=collated_global_node_ids,
        target_node_mask=collated_target_node_mask,
        target_hyperedge_mask=collated_target_hyperedge_mask,
        y=collated_y,
        task=self.__cached_dataset_hdata.task,
    )

    return collated_hdata.to(batch[0].device)

__collate_y_and_target_masks_for_task(batch, hyperedge_index_wrapper)

Collates the labels (y) and target masks for a batch of HData instances based on the task type.

Parameters:

Name Type Description Default
batch list[HData]

List of HData instances containing the data to collate.

required
hyperedge_index_wrapper HyperedgeIndex

A HyperedgeIndex wrapping the collated hyperedge index.

required

Returns:

Name Type Description
collated_y Tensor

A tensor containing the collated labels for the batch.

collated_target_node_mask Tensor | None

A tensor containing the collated target node mask for the batch, or None if not applicable.

collated_target_hyperedge_mask Tensor | None

A tensor containing the collated target hyperedge mask for the batch, or None if not applicable.

Source code in hypertorch/data/loader.py
def __collate_y_and_target_masks_for_task(
    self,
    batch: list[HData],
    hyperedge_index_wrapper: HyperedgeIndex,
) -> tuple[Tensor, Tensor | None, Tensor | None]:
    """
    Collates the labels (y) and target masks for a batch of
    HData instances based on the task type.

    Args:
        batch: List of HData instances containing the data to collate.
        hyperedge_index_wrapper: A HyperedgeIndex wrapping the collated hyperedge index.

    Returns:
        collated_y: A tensor containing the collated labels for the batch.
        collated_target_node_mask: A tensor containing the collated target node mask
            for the batch, or ``None`` if not applicable.
        collated_target_hyperedge_mask: A tensor containing the collated target hyperedge
            mask for the batch, or ``None`` if not applicable.
    """
    node_ids = hyperedge_index_wrapper.node_ids
    hyperedge_ids = hyperedge_index_wrapper.hyperedge_ids

    if self.__cached_dataset_hdata.is_node_related_task:
        collated_y = self.__cached_dataset_hdata.y[node_ids]
        collated_target_hyperedge_mask = None

        target_node_ids_list = [
            HyperedgeIndex(hdata.hyperedge_index).node_ids[hdata.target_node_mask]
            for hdata in batch
        ]
        target_node_ids = torch.cat(target_node_ids_list, dim=0)
        collated_target_node_mask = torch.isin(node_ids, target_node_ids)
    elif self.__cached_dataset_hdata.is_hyperedge_related_task:
        collated_y = self.__cached_dataset_hdata.y[hyperedge_ids]
        collated_target_node_mask = None

        target_hyperedge_ids_list = [
            HyperedgeIndex(hdata.hyperedge_index).hyperedge_ids[hdata.target_hyperedge_mask]
            for hdata in batch
        ]
        target_hyperedge_ids = torch.cat(target_hyperedge_ids_list, dim=0)
        collated_target_hyperedge_mask = torch.isin(hyperedge_ids, target_hyperedge_ids)
    else:
        raise ValueError(
            f"Unsupported task category for task={self.__cached_dataset_hdata.task!r}."
        )

    return collated_y, collated_target_node_mask, collated_target_hyperedge_mask

ABHyperedgeWeightsEnricher

Bases: HyperedgeWeightsEnricher

Generates hyperedge weights based on the number of nodes in each hyperedge.

Attributes:

Name Type Description
alpha float

Scaling factor for the random component added to weights. Must be between 0.0 and 1.0.

beta float | None

If provided, the random component is alpha * beta. If None, no random component is added.

Source code in hypertorch/data/enricher.py
class ABHyperedgeWeightsEnricher(HyperedgeWeightsEnricher):
    """
    Generates hyperedge weights based on the number of nodes in each hyperedge.

    Attributes:
        alpha: Scaling factor for the random component added to weights.
            Must be between ``0.0`` and ``1.0``.
        beta: If provided, the random component is alpha * beta.
            If ``None``, no random component is added.
    """

    def __init__(
        self,
        cache_dir: str | None = None,
        alpha: float = 1.0,
        beta: float | None = None,
    ):
        """
        Initialize the hyperedge weight enricher.

        Args:
            cache_dir: Directory for saving/loading cached features.
                If ``None``, caching is disabled.
            alpha: Scaling factor for the random component added to weights.
            beta: If provided, the random component is ``alpha * beta``.

        Raises:
            ValueError: If ``alpha`` or ``beta`` is invalid.
        """
        super().__init__(cache_dir=cache_dir)

        validate_is_between("alpha", alpha, 0.0, 1.0)
        validate_is_finite_when_provided("beta", beta)

        self.alpha: float = alpha
        self.beta: float | None = beta

    def enrich(self, hyperedge_index: Tensor) -> Tensor:
        """
        Compute edge weights as the number of nodes in each hyperedge.

        Args:
            hyperedge_index: Hyperedge index tensor of shape ``(2, num_hyperedges)``.

        Returns:
            hyperedge_weight: Tensor of shape ``(num_hyperedges,)`` containing
                the weight of each hyperedge.
        """
        # Count the number of nodes in each hyperedge by counting occurrences of
        # each hyperedge index.
        # Example: if hyperedge_index[1] = [0, 0, 1, 1, 1], then we have 2 nodes
        # in hyperedge 0 and 3 nodes in hyperedge 1.
        num_hyperedges = int(hyperedge_index[1].max().item()) + 1
        weights = torch.bincount(hyperedge_index[1], minlength=num_hyperedges).float()

        random_alpha = random.uniform(0, self.alpha)
        if self.beta is not None:
            weights += random_alpha * self.beta
        return weights

__init__(cache_dir=None, alpha=1.0, beta=None)

Initialize the hyperedge weight enricher.

Parameters:

Name Type Description Default
cache_dir str | None

Directory for saving/loading cached features. If None, caching is disabled.

None
alpha float

Scaling factor for the random component added to weights.

1.0
beta float | None

If provided, the random component is alpha * beta.

None

Raises:

Type Description
ValueError

If alpha or beta is invalid.

Source code in hypertorch/data/enricher.py
def __init__(
    self,
    cache_dir: str | None = None,
    alpha: float = 1.0,
    beta: float | None = None,
):
    """
    Initialize the hyperedge weight enricher.

    Args:
        cache_dir: Directory for saving/loading cached features.
            If ``None``, caching is disabled.
        alpha: Scaling factor for the random component added to weights.
        beta: If provided, the random component is ``alpha * beta``.

    Raises:
        ValueError: If ``alpha`` or ``beta`` is invalid.
    """
    super().__init__(cache_dir=cache_dir)

    validate_is_between("alpha", alpha, 0.0, 1.0)
    validate_is_finite_when_provided("beta", beta)

    self.alpha: float = alpha
    self.beta: float | None = beta

enrich(hyperedge_index)

Compute edge weights as the number of nodes in each hyperedge.

Parameters:

Name Type Description Default
hyperedge_index Tensor

Hyperedge index tensor of shape (2, num_hyperedges).

required

Returns:

Name Type Description
hyperedge_weight Tensor

Tensor of shape (num_hyperedges,) containing the weight of each hyperedge.

Source code in hypertorch/data/enricher.py
def enrich(self, hyperedge_index: Tensor) -> Tensor:
    """
    Compute edge weights as the number of nodes in each hyperedge.

    Args:
        hyperedge_index: Hyperedge index tensor of shape ``(2, num_hyperedges)``.

    Returns:
        hyperedge_weight: Tensor of shape ``(num_hyperedges,)`` containing
            the weight of each hyperedge.
    """
    # Count the number of nodes in each hyperedge by counting occurrences of
    # each hyperedge index.
    # Example: if hyperedge_index[1] = [0, 0, 1, 1, 1], then we have 2 nodes
    # in hyperedge 0 and 3 nodes in hyperedge 1.
    num_hyperedges = int(hyperedge_index[1].max().item()) + 1
    weights = torch.bincount(hyperedge_index[1], minlength=num_hyperedges).float()

    random_alpha = random.uniform(0, self.alpha)
    if self.beta is not None:
        weights += random_alpha * self.beta
    return weights

FillValueHyperedgeAttrsEnricher

Bases: HyperedgeAttrsEnricher

Generates simple hyperedge attributes by filling them with a constant value.

Attributes:

Name Type Description
fill_value float

The constant value to fill the hyperedge attributes with. Defaults to 1.0.

Source code in hypertorch/data/enricher.py
class FillValueHyperedgeAttrsEnricher(HyperedgeAttrsEnricher):
    """
    Generates simple hyperedge attributes by filling them with a constant value.

    Attributes:
        fill_value: The constant value to fill the hyperedge attributes with. Defaults to ``1.0``.
    """

    def __init__(
        self,
        cache_dir: str | None = None,
        fill_value: float = 1.0,
    ):
        """
        Initialize the fill-value hyperedge attribute enricher.

        Args:
            cache_dir: Directory for saving/loading cached features.
                If ``None``, caching is disabled.
            fill_value: The constant value to fill the hyperedge attributes with.
        """
        super().__init__(cache_dir=cache_dir)
        self.fill_value: float = fill_value

    def enrich(self, hyperedge_index: Tensor) -> Tensor:
        """
        Generate hyperedge attributes.

        Args:
            hyperedge_index: Hyperedge index tensor of shape ``(2, num_hyperedges)``.

        Returns:
            hyperedge_attr: Tensor of shape ``(num_hyperedges, 1)`` containing
                the generated attribute for each hyperedge.
        """
        num_hyperedges = HyperedgeIndex(hyperedge_index).num_hyperedges
        hyperedge_attrs = torch.full(
            size=(num_hyperedges, 1),
            fill_value=self.fill_value,
            dtype=torch.float,
            device=hyperedge_index.device,
        )
        return hyperedge_attrs

__init__(cache_dir=None, fill_value=1.0)

Initialize the fill-value hyperedge attribute enricher.

Parameters:

Name Type Description Default
cache_dir str | None

Directory for saving/loading cached features. If None, caching is disabled.

None
fill_value float

The constant value to fill the hyperedge attributes with.

1.0
Source code in hypertorch/data/enricher.py
def __init__(
    self,
    cache_dir: str | None = None,
    fill_value: float = 1.0,
):
    """
    Initialize the fill-value hyperedge attribute enricher.

    Args:
        cache_dir: Directory for saving/loading cached features.
            If ``None``, caching is disabled.
        fill_value: The constant value to fill the hyperedge attributes with.
    """
    super().__init__(cache_dir=cache_dir)
    self.fill_value: float = fill_value

enrich(hyperedge_index)

Generate hyperedge attributes.

Parameters:

Name Type Description Default
hyperedge_index Tensor

Hyperedge index tensor of shape (2, num_hyperedges).

required

Returns:

Name Type Description
hyperedge_attr Tensor

Tensor of shape (num_hyperedges, 1) containing the generated attribute for each hyperedge.

Source code in hypertorch/data/enricher.py
def enrich(self, hyperedge_index: Tensor) -> Tensor:
    """
    Generate hyperedge attributes.

    Args:
        hyperedge_index: Hyperedge index tensor of shape ``(2, num_hyperedges)``.

    Returns:
        hyperedge_attr: Tensor of shape ``(num_hyperedges, 1)`` containing
            the generated attribute for each hyperedge.
    """
    num_hyperedges = HyperedgeIndex(hyperedge_index).num_hyperedges
    hyperedge_attrs = torch.full(
        size=(num_hyperedges, 1),
        fill_value=self.fill_value,
        dtype=torch.float,
        device=hyperedge_index.device,
    )
    return hyperedge_attrs

HyperedgeEnricher

Bases: Enricher, ABC

Base class for hyperedge enrichers.

Source code in hypertorch/data/enricher.py
class HyperedgeEnricher(Enricher, ABC):
    """
    Base class for hyperedge enrichers.
    """

    pass

LaplacianPositionalEncodingEnricher

Bases: NodeEnricher

Enrich node features with Laplacian Positional Encodings computed from the symmetric normalized Laplacian of the clique expansion of the hypergraph.

Attributes:

Name Type Description
num_features int

Number of positional encoding features to generate for each node.

num_nodes int

Total number of nodes in the graph. If not provided, it will be inferred from hyperedge_index. This is only needed if hyperedge_index does not include all nodes (e.g., some isolated nodes are missing). Another instance is when the setting is transductive and hyperedge_index contains some hyperedges that do not contain all the nodes in the node space.

Source code in hypertorch/data/enricher.py
class LaplacianPositionalEncodingEnricher(NodeEnricher):
    """
    Enrich node features with Laplacian Positional Encodings computed from the symmetric normalized
    Laplacian of the clique expansion of the hypergraph.

    Attributes:
        num_features: Number of positional encoding features to generate for each node.
        num_nodes: Total number of nodes in the graph. If not provided, it will be inferred
            from ``hyperedge_index``. This is only needed if ``hyperedge_index`` does not include
            all nodes (e.g., some isolated nodes are missing). Another instance is when the setting
            is transductive and ``hyperedge_index`` contains some hyperedges that do not contain
            all the nodes in the node space.
    """

    def __init__(
        self,
        num_features: int,
        num_nodes: int = 0,
        cache_dir: str | None = None,
    ):
        """
        Initialize the Laplacian positional encoding enricher.

        Args:
            num_features: Number of positional encoding features to generate for each node.
            num_nodes: Total number of nodes in the graph.
                If not provided, it is inferred from ``hyperedge_index``.
            cache_dir: Optional directory to cache computed features.
                If ``None``, caching is disabled.

        Raises:
            ValueError: If ``num_features`` or ``num_nodes`` is invalid.
        """
        super().__init__(cache_dir=cache_dir)

        validate_is_positive("num_features", num_features)
        validate_is_non_negative("num_nodes", num_nodes)

        self.num_features: int = num_features
        self.num_nodes: int = num_nodes

    def enrich(self, hyperedge_index: Tensor) -> Tensor:
        """
        Compute Laplacian Positional Encoding: the k smallest non-trivial eigenvectors
        of the symmetric normalized Laplacian L = I - D^{-1/2} A D^{-1/2}.

        The first eigenvector (constant, eigenvalue ~0) is skipped.
        The next num_features eigenvectors are returned as positional features.

        Args:
            hyperedge_index: Hyperedge index tensor of shape ``(2, num_hyperedges)``.

        Returns:
            node_features: Tensor of shape ``(num_nodes, num_features)``.
        """
        num_nodes = self.num_nodes if self.num_nodes > 0 else None
        edge_index = HyperedgeIndex(hyperedge_index).reduce_to_edge_index_on_clique_expansion(
            num_nodes=num_nodes
        )
        edge_index_wrapper = EdgeIndex(edge_index)
        laplacian_matrix = edge_index_wrapper.get_sparse_normalized_laplacian(num_nodes=num_nodes)
        laplacian_matrix_dense = (
            laplacian_matrix.to_dense()  # torch.linalg.eigh only works on dense tensors
        )

        # Compute eigenvalues and eigenvectors of the symmetric Laplacian.
        # torch.linalg.eigh returns them sorted in ascending order of eigenvalue.
        # The smallest eigenvalue is ~0 with a constant eigenvector (all entries equal),
        # which carries no positional information and will be skipped.
        # Example: eigenvalues ~ [0, 1, 2],
        #          eigenvectors ~ [[0.577, -0.707, 0.408],
        #                          [0.577,  0.000, -0.816],
        #                          [0.577,  0.707,  0.408]]
        # Column 0 (eigenvalue ~0) is the trivial constant vector, all entries ~0.577.
        # eigenvectors shape is ``(num_nodes, num_nodes)``, each column is an eigenvector.
        with torch.no_grad():
            _, eigenvectors = torch.linalg.eigh(laplacian_matrix_dense)
            eigenvectors = cast(Tensor, eigenvectors)

        # We skip the first (trivial) eigenvector, so at most num_nodes - 1 are usable.
        # Example: 3 nodes -> 2 available non-trivial eigenvectors
        num_nodes = int(eigenvectors.size(0))
        num_nontrivial_eigenvectors = num_nodes - 1

        # If we have enough eigenvectors, slice columns 1 through num_features (inclusive).
        # Each row will be the positional encoding for that node.
        # Example: num_features = 2, eigenvectors.shape = (3, 3)
        #          -> return columns 1 and 2
        #             shape (3, 2)  # (num_nodes, num_features)
        if num_nontrivial_eigenvectors >= self.num_features:
            return eigenvectors[:, 1 : self.num_features + 1]

        # If the graph has fewer usable eigenvectors than requested
        # (e.g., num_features = 5 but only 2 available), we create a zero-padded tensor
        # and fill what we have.
        # Example: num_nontrivial_eigenvectors = 2, num_features = 5
        #          -> shape (3, 5)  # columns 0-1 filled, 2-4 are zeros.
        x = torch.zeros(
            size=(num_nodes, self.num_features),
            dtype=eigenvectors.dtype,
            device=eigenvectors.device,
        )
        x[:, :num_nontrivial_eigenvectors] = eigenvectors[:, 1:]
        return x

__init__(num_features, num_nodes=0, cache_dir=None)

Initialize the Laplacian positional encoding enricher.

Parameters:

Name Type Description Default
num_features int

Number of positional encoding features to generate for each node.

required
num_nodes int

Total number of nodes in the graph. If not provided, it is inferred from hyperedge_index.

0
cache_dir str | None

Optional directory to cache computed features. If None, caching is disabled.

None

Raises:

Type Description
ValueError

If num_features or num_nodes is invalid.

Source code in hypertorch/data/enricher.py
def __init__(
    self,
    num_features: int,
    num_nodes: int = 0,
    cache_dir: str | None = None,
):
    """
    Initialize the Laplacian positional encoding enricher.

    Args:
        num_features: Number of positional encoding features to generate for each node.
        num_nodes: Total number of nodes in the graph.
            If not provided, it is inferred from ``hyperedge_index``.
        cache_dir: Optional directory to cache computed features.
            If ``None``, caching is disabled.

    Raises:
        ValueError: If ``num_features`` or ``num_nodes`` is invalid.
    """
    super().__init__(cache_dir=cache_dir)

    validate_is_positive("num_features", num_features)
    validate_is_non_negative("num_nodes", num_nodes)

    self.num_features: int = num_features
    self.num_nodes: int = num_nodes

enrich(hyperedge_index)

Compute Laplacian Positional Encoding: the k smallest non-trivial eigenvectors of the symmetric normalized Laplacian L = I - D^{-½} A D^{-½}.

The first eigenvector (constant, eigenvalue ~0) is skipped. The next num_features eigenvectors are returned as positional features.

Parameters:

Name Type Description Default
hyperedge_index Tensor

Hyperedge index tensor of shape (2, num_hyperedges).

required

Returns:

Name Type Description
node_features Tensor

Tensor of shape (num_nodes, num_features).

Source code in hypertorch/data/enricher.py
def enrich(self, hyperedge_index: Tensor) -> Tensor:
    """
    Compute Laplacian Positional Encoding: the k smallest non-trivial eigenvectors
    of the symmetric normalized Laplacian L = I - D^{-1/2} A D^{-1/2}.

    The first eigenvector (constant, eigenvalue ~0) is skipped.
    The next num_features eigenvectors are returned as positional features.

    Args:
        hyperedge_index: Hyperedge index tensor of shape ``(2, num_hyperedges)``.

    Returns:
        node_features: Tensor of shape ``(num_nodes, num_features)``.
    """
    num_nodes = self.num_nodes if self.num_nodes > 0 else None
    edge_index = HyperedgeIndex(hyperedge_index).reduce_to_edge_index_on_clique_expansion(
        num_nodes=num_nodes
    )
    edge_index_wrapper = EdgeIndex(edge_index)
    laplacian_matrix = edge_index_wrapper.get_sparse_normalized_laplacian(num_nodes=num_nodes)
    laplacian_matrix_dense = (
        laplacian_matrix.to_dense()  # torch.linalg.eigh only works on dense tensors
    )

    # Compute eigenvalues and eigenvectors of the symmetric Laplacian.
    # torch.linalg.eigh returns them sorted in ascending order of eigenvalue.
    # The smallest eigenvalue is ~0 with a constant eigenvector (all entries equal),
    # which carries no positional information and will be skipped.
    # Example: eigenvalues ~ [0, 1, 2],
    #          eigenvectors ~ [[0.577, -0.707, 0.408],
    #                          [0.577,  0.000, -0.816],
    #                          [0.577,  0.707,  0.408]]
    # Column 0 (eigenvalue ~0) is the trivial constant vector, all entries ~0.577.
    # eigenvectors shape is ``(num_nodes, num_nodes)``, each column is an eigenvector.
    with torch.no_grad():
        _, eigenvectors = torch.linalg.eigh(laplacian_matrix_dense)
        eigenvectors = cast(Tensor, eigenvectors)

    # We skip the first (trivial) eigenvector, so at most num_nodes - 1 are usable.
    # Example: 3 nodes -> 2 available non-trivial eigenvectors
    num_nodes = int(eigenvectors.size(0))
    num_nontrivial_eigenvectors = num_nodes - 1

    # If we have enough eigenvectors, slice columns 1 through num_features (inclusive).
    # Each row will be the positional encoding for that node.
    # Example: num_features = 2, eigenvectors.shape = (3, 3)
    #          -> return columns 1 and 2
    #             shape (3, 2)  # (num_nodes, num_features)
    if num_nontrivial_eigenvectors >= self.num_features:
        return eigenvectors[:, 1 : self.num_features + 1]

    # If the graph has fewer usable eigenvectors than requested
    # (e.g., num_features = 5 but only 2 available), we create a zero-padded tensor
    # and fill what we have.
    # Example: num_nontrivial_eigenvectors = 2, num_features = 5
    #          -> shape (3, 5)  # columns 0-1 filled, 2-4 are zeros.
    x = torch.zeros(
        size=(num_nodes, self.num_features),
        dtype=eigenvectors.dtype,
        device=eigenvectors.device,
    )
    x[:, :num_nontrivial_eigenvectors] = eigenvectors[:, 1:]
    return x

Node2VecEnricher

Bases: NodeEnricher

Enrich node features using Node2Vec embeddings computed from the clique expansion of the hypergraph.

Attributes:

Name Type Description
embedding_dim int

Dimensionality of the node embeddings to generate.

walk_length int

Length of each random walk.

context_size int

Window size for the skip-gram model (number of neighbors in the walk considered as context). For example, if context_size=2 and walk_length=5, then for a random walk [v0, v1, v2, v3, v4], the context for v2 would be [v0, v1, v3, v4] as we take neighbors within distance 2 in the walk. The pairs generated by skip-gram would be [(v2, v0), (v2, v1), (v2, v3), (v2, v4)]. Rule of thumb: Graphs with strong local structure (5-10), Graphs with communities/long-range patterns (10-20). Defaults to 10.

num_walks_per_node int

Number of random walks to start at each node.

p float

Return hyperparameter for Node2Vec. Default is 1.0 (unbiased). This controls the probability of stepping back to the node visited in the previous step. Lower values of p make immediate backtracking more likely, which keeps walks closer to the local neighborhood. Higher values of p discourage returning to the previous node, so walks are less likely to bounce back and forth across the same edge.

q float

In-out hyperparameter for Node2Vec. Default is 1.0 (unbiased). This controls whether walks stay near the source node or explore further outward. Lower values of q bias the walk toward outward exploration, behaving more like DFS and emphasizing structural roles. Higher values of q bias the walk toward nearby nodes, behaving more like BFS and emphasizing community structure and homophily.

num_negative_samples int

Number of negative samples used for skip-gram training. If set to X, then for each positive pair (u, v) generated from the random walks, X negative pairs (u, v_neg) will be generated, where v_neg is a node sampled uniformly at random from all nodes in the graph. Defaults to 1, meaning one negative sample per positive pair.

num_nodes int

Total number of nodes to preserve. If not provided, it will be inferred from hyperedge_index. This is only needed if hyperedge_index does not include all nodes (e.g., some isolated nodes are missing).

graph_reduction_strategy GraphReductionStrategy

Strategy for reducing the hyperedge graph. Defaults to clique_expansion.

num_epochs int

Number of epochs used to optimize Node2Vec embeddings. Defaults to 5.

learning_rate float

Learning rate for embedding optimization. Defaults to 0.01.

batch_size int

Batch size used by the random-walk loader. Defaults to 128.

sparse bool

Whether Node2Vec embeddings should use sparse gradients.

verbose bool

Whether to print verbose output during training. Defaults to False.

Source code in hypertorch/data/enricher.py
class Node2VecEnricher(NodeEnricher):
    """
    Enrich node features using Node2Vec embeddings computed from the clique expansion of the
    hypergraph.

    Attributes:
        embedding_dim: Dimensionality of the node embeddings to generate.
        walk_length: Length of each random walk.
        context_size: Window size for the skip-gram model
            (number of neighbors in the walk considered as context).
            For example, if ``context_size=2`` and ``walk_length=5``, then for
            a random walk ``[v0, v1, v2, v3, v4]``,
            the context for ``v2`` would be ``[v0, v1, v3, v4]`` as we take neighbors within
            distance 2 in the walk.
            The pairs generated by skip-gram would be ``[(v2, v0), (v2, v1), (v2, v3), (v2, v4)]``.
            Rule of thumb: Graphs with strong local structure (5-10), Graphs with
            communities/long-range patterns (10-20).
            Defaults to ``10``.
        num_walks_per_node: Number of random walks to start at each node.
        p: Return hyperparameter for Node2Vec. Default is ``1.0`` (unbiased).
            This controls the probability of stepping back to the node visited in the previous step.
            Lower values of ``p`` make immediate backtracking more likely,
            which keeps walks closer to the local neighborhood. Higher values of ``p`` discourage
            returning to the previous node, so walks are less likely to bounce back
            and forth across the same edge.
        q: In-out hyperparameter for Node2Vec. Default is ``1.0`` (unbiased).
            This controls whether walks stay near the source node or explore further outward.
            Lower values of ``q`` bias the walk toward outward exploration, behaving more like DFS
            and emphasizing structural roles. Higher values of ``q`` bias the walk toward
            nearby nodes, behaving more like BFS and emphasizing community structure and homophily.
        num_negative_samples: Number of negative samples used for skip-gram training.
            If set to ``X``, then for each positive pair ``(u, v)`` generated from the random walks,
            ``X`` negative pairs ``(u, v_neg)`` will be generated,
            where ``v_neg`` is a node sampled uniformly at random from all nodes in the graph.
            Defaults to ``1``, meaning one negative sample per positive pair.
        num_nodes: Total number of nodes to preserve. If not provided, it will be inferred from
            ``hyperedge_index``. This is only needed if ``hyperedge_index`` does not include
            all nodes (e.g., some isolated nodes are missing).
        graph_reduction_strategy: Strategy for reducing the hyperedge graph.
            Defaults to ``clique_expansion``.
        num_epochs: Number of epochs used to optimize Node2Vec embeddings. Defaults to ``5``.
        learning_rate: Learning rate for embedding optimization. Defaults to ``0.01``.
        batch_size: Batch size used by the random-walk loader. Defaults to ``128``.
        sparse: Whether Node2Vec embeddings should use sparse gradients.
        verbose: Whether to print verbose output during training. Defaults to ``False``.
    """

    def __init__(
        self,
        num_features: int,
        walk_length: int = 20,
        context_size: int = 10,
        num_walks_per_node: int = 10,
        p: float = 1.0,
        q: float = 1.0,
        num_negative_samples: int = 1,
        num_nodes: int = 0,
        graph_reduction_strategy: GraphReductionStrategy = (
            GraphReductionStrategyEnum.CLIQUE_EXPANSION
        ),
        num_epochs: int = 5,
        learning_rate: float = 0.01,
        batch_size: int = 128,
        sparse: bool = True,
        cache_dir: str | None = None,
        verbose: bool = False,
    ):
        """
        Initialize the Node2Vec enricher.

        Args:
            num_features: Dimensionality of the node embeddings to generate.
            walk_length: Length of each random walk.
            context_size: Window size for the skip-gram model. For example, if
                ``context_size=2`` and ``walk_length=5``, then for a random walk
                ``[v0, v1, v2, v3, v4]``, the context for ``v2`` is
                ``[v0, v1, v3, v4]``.
            num_walks_per_node: Number of random walks to start at each node.
            p: Return hyperparameter for Node2Vec.
            q: In-out hyperparameter for Node2Vec.
            num_negative_samples: Number of negative samples to use for skip-gram training.
            num_nodes: Total number of nodes in the graph. If not provided, it is inferred from
                ``hyperedge_index``.
            graph_reduction_strategy: Strategy for reducing the hypergraph.
            num_epochs: Number of epochs used to optimize Node2Vec embeddings.
            learning_rate: Learning rate for embedding optimization.
            batch_size: Batch size used by the random-walk loader.
            sparse: Whether Node2Vec embeddings should use sparse gradients.
            cache_dir: Optional directory to cache computed embeddings.
                If ``None``, caching is disabled.
            verbose: Whether to print verbose output during training.
        """
        super().__init__(cache_dir=cache_dir)
        self.embedding_dim: int = num_features
        self.walk_length: int = walk_length
        self.context_size: int = context_size
        self.num_walks_per_node: int = num_walks_per_node
        self.p: float = p
        self.q: float = q
        self.num_negative_samples: int = num_negative_samples
        self.num_nodes: int = num_nodes
        self.graph_reduction_strategy: GraphReductionStrategy = graph_reduction_strategy
        self.num_epochs: int = num_epochs
        self.learning_rate: float = learning_rate
        self.batch_size: int = batch_size
        self.sparse: bool = sparse
        self.verbose: bool = verbose

        self.__validate()

    def enrich(self, hyperedge_index: Tensor) -> Tensor:
        """
        Compute Node2Vec embeddings from the clique expansion of the hypergraph.

        The hypergraph is converted to a regular graph via clique expansion, where each hyperedge
        of size k contributes a k x k block of edges between its member nodes.
        The resulting ``edge_index`` is then used to train a Node2Vec model using random walks
        and the skip-gram objective.

        Args:
            hyperedge_index: Hyperedge index tensor of shape ``(2, num_hyperedges)``.

        Returns:
            x: Tensor of shape ``(num_nodes, embedding_dim)`` containing the Node2Vec embeddings
                for each node.
        """
        device = hyperedge_index.device

        if self.verbose:
            print(f"Reducing hypergraph to graph via {self.graph_reduction_strategy}...")

        hyperedge_index_wrapper = HyperedgeIndex(hyperedge_index)
        num_nodes = hyperedge_index_wrapper.num_nodes_if_isolated_exist(self.num_nodes)
        if num_nodes == 0:
            warnings.warn(
                "Found no nodes. Returning empty node features.",
                category=UserWarning,
                stacklevel=2,
            )
            return torch.empty(size=(0, self.embedding_dim), dtype=torch.float, device=device)

        reduced_edge_index = hyperedge_index_wrapper.reduce(
            self.graph_reduction_strategy,
            num_nodes=num_nodes,
        )
        edge_index_wrapper = EdgeIndex(reduced_edge_index).remove_selfloops()
        if edge_index_wrapper.num_edges == 0:
            warnings.warn(
                """
                Clique expansion produced no non-self-loop edges. Returning zero node features.
                """,
                category=UserWarning,
                stacklevel=2,
            )
            return torch.zeros(
                size=(num_nodes, self.embedding_dim),
                dtype=torch.float,
                device=device,
            )

        edge_index = edge_index_wrapper.item.to(device)
        model = PyGNode2Vec(
            edge_index=edge_index,
            embedding_dim=self.embedding_dim,
            walk_length=self.walk_length,
            context_size=self.context_size,
            walks_per_node=self.num_walks_per_node,
            p=self.p,
            q=self.q,
            num_negative_samples=self.num_negative_samples,
            num_nodes=num_nodes,
            sparse=self.sparse,
        ).to(device)

        data_loader = model.loader(batch_size=self.batch_size, shuffle=True)
        optimizer = (
            optim.SparseAdam(model.parameters(), lr=self.learning_rate)
            if self.sparse
            else optim.Adam(model.parameters(), lr=self.learning_rate)
        )

        if self.verbose:
            print(f"Training Node2Vec model for {self.num_epochs} epochs...")

        model.train()
        for epoch in range(self.num_epochs):
            if self.verbose:
                print(f"Epoch {epoch + 1}/{self.num_epochs}\r", end="")

            # Iterate over batches of positive and negative random walks
            for positive_random_walk, negative_random_walk in data_loader:
                positive_random_walk_on_device = positive_random_walk.to(device)
                negative_random_walk_on_device = negative_random_walk.to(device)

                optimizer.zero_grad()
                loss = model.loss(positive_random_walk_on_device, negative_random_walk_on_device)
                loss.backward()
                optimizer.step()

        if self.verbose:
            print("Training complete. Generating node embeddings...")

        model.eval()
        with torch.no_grad():
            x: Tensor = model()  # shape (num_nodes, num_features)

        # Detach node embeddings from computation graph and return them
        return x.detach().to(device)

    def __validate(self) -> None:
        """
        Validate Node2Vec enrichment configuration.

        Raises:
            ValueError: If any configuration value is outside its supported range.
        """
        validate_is_positive("num_features", self.embedding_dim)
        validate_is_positive("walk_length", self.walk_length)
        validate_is_positive("context_size", self.context_size)
        if self.walk_length < self.context_size:
            raise ValueError(
                "Expected walk_length >= context_size, got "
                f"walk_length={self.walk_length}, context_size={self.context_size}."
            )

        validate_is_positive("num_walks_per_node", self.num_walks_per_node)
        validate_is_finite("p", self.p)
        validate_is_positive("p", self.p)
        validate_is_finite("q", self.q)
        validate_is_positive("q", self.q)
        validate_is_positive("num_negative_samples", self.num_negative_samples)
        validate_is_non_negative("num_nodes", self.num_nodes)
        validate_is_positive("num_epochs", self.num_epochs)
        validate_is_finite("learning_rate", self.learning_rate)
        validate_is_positive("learning_rate", self.learning_rate)
        validate_is_positive("batch_size", self.batch_size)

__init__(num_features, walk_length=20, context_size=10, num_walks_per_node=10, p=1.0, q=1.0, num_negative_samples=1, num_nodes=0, graph_reduction_strategy=GraphReductionStrategyEnum.CLIQUE_EXPANSION, num_epochs=5, learning_rate=0.01, batch_size=128, sparse=True, cache_dir=None, verbose=False)

Initialize the Node2Vec enricher.

Parameters:

Name Type Description Default
num_features int

Dimensionality of the node embeddings to generate.

required
walk_length int

Length of each random walk.

20
context_size int

Window size for the skip-gram model. For example, if context_size=2 and walk_length=5, then for a random walk [v0, v1, v2, v3, v4], the context for v2 is [v0, v1, v3, v4].

10
num_walks_per_node int

Number of random walks to start at each node.

10
p float

Return hyperparameter for Node2Vec.

1.0
q float

In-out hyperparameter for Node2Vec.

1.0
num_negative_samples int

Number of negative samples to use for skip-gram training.

1
num_nodes int

Total number of nodes in the graph. If not provided, it is inferred from hyperedge_index.

0
graph_reduction_strategy GraphReductionStrategy

Strategy for reducing the hypergraph.

CLIQUE_EXPANSION
num_epochs int

Number of epochs used to optimize Node2Vec embeddings.

5
learning_rate float

Learning rate for embedding optimization.

0.01
batch_size int

Batch size used by the random-walk loader.

128
sparse bool

Whether Node2Vec embeddings should use sparse gradients.

True
cache_dir str | None

Optional directory to cache computed embeddings. If None, caching is disabled.

None
verbose bool

Whether to print verbose output during training.

False
Source code in hypertorch/data/enricher.py
def __init__(
    self,
    num_features: int,
    walk_length: int = 20,
    context_size: int = 10,
    num_walks_per_node: int = 10,
    p: float = 1.0,
    q: float = 1.0,
    num_negative_samples: int = 1,
    num_nodes: int = 0,
    graph_reduction_strategy: GraphReductionStrategy = (
        GraphReductionStrategyEnum.CLIQUE_EXPANSION
    ),
    num_epochs: int = 5,
    learning_rate: float = 0.01,
    batch_size: int = 128,
    sparse: bool = True,
    cache_dir: str | None = None,
    verbose: bool = False,
):
    """
    Initialize the Node2Vec enricher.

    Args:
        num_features: Dimensionality of the node embeddings to generate.
        walk_length: Length of each random walk.
        context_size: Window size for the skip-gram model. For example, if
            ``context_size=2`` and ``walk_length=5``, then for a random walk
            ``[v0, v1, v2, v3, v4]``, the context for ``v2`` is
            ``[v0, v1, v3, v4]``.
        num_walks_per_node: Number of random walks to start at each node.
        p: Return hyperparameter for Node2Vec.
        q: In-out hyperparameter for Node2Vec.
        num_negative_samples: Number of negative samples to use for skip-gram training.
        num_nodes: Total number of nodes in the graph. If not provided, it is inferred from
            ``hyperedge_index``.
        graph_reduction_strategy: Strategy for reducing the hypergraph.
        num_epochs: Number of epochs used to optimize Node2Vec embeddings.
        learning_rate: Learning rate for embedding optimization.
        batch_size: Batch size used by the random-walk loader.
        sparse: Whether Node2Vec embeddings should use sparse gradients.
        cache_dir: Optional directory to cache computed embeddings.
            If ``None``, caching is disabled.
        verbose: Whether to print verbose output during training.
    """
    super().__init__(cache_dir=cache_dir)
    self.embedding_dim: int = num_features
    self.walk_length: int = walk_length
    self.context_size: int = context_size
    self.num_walks_per_node: int = num_walks_per_node
    self.p: float = p
    self.q: float = q
    self.num_negative_samples: int = num_negative_samples
    self.num_nodes: int = num_nodes
    self.graph_reduction_strategy: GraphReductionStrategy = graph_reduction_strategy
    self.num_epochs: int = num_epochs
    self.learning_rate: float = learning_rate
    self.batch_size: int = batch_size
    self.sparse: bool = sparse
    self.verbose: bool = verbose

    self.__validate()

enrich(hyperedge_index)

Compute Node2Vec embeddings from the clique expansion of the hypergraph.

The hypergraph is converted to a regular graph via clique expansion, where each hyperedge of size k contributes a k x k block of edges between its member nodes. The resulting edge_index is then used to train a Node2Vec model using random walks and the skip-gram objective.

Parameters:

Name Type Description Default
hyperedge_index Tensor

Hyperedge index tensor of shape (2, num_hyperedges).

required

Returns:

Name Type Description
x Tensor

Tensor of shape (num_nodes, embedding_dim) containing the Node2Vec embeddings for each node.

Source code in hypertorch/data/enricher.py
def enrich(self, hyperedge_index: Tensor) -> Tensor:
    """
    Compute Node2Vec embeddings from the clique expansion of the hypergraph.

    The hypergraph is converted to a regular graph via clique expansion, where each hyperedge
    of size k contributes a k x k block of edges between its member nodes.
    The resulting ``edge_index`` is then used to train a Node2Vec model using random walks
    and the skip-gram objective.

    Args:
        hyperedge_index: Hyperedge index tensor of shape ``(2, num_hyperedges)``.

    Returns:
        x: Tensor of shape ``(num_nodes, embedding_dim)`` containing the Node2Vec embeddings
            for each node.
    """
    device = hyperedge_index.device

    if self.verbose:
        print(f"Reducing hypergraph to graph via {self.graph_reduction_strategy}...")

    hyperedge_index_wrapper = HyperedgeIndex(hyperedge_index)
    num_nodes = hyperedge_index_wrapper.num_nodes_if_isolated_exist(self.num_nodes)
    if num_nodes == 0:
        warnings.warn(
            "Found no nodes. Returning empty node features.",
            category=UserWarning,
            stacklevel=2,
        )
        return torch.empty(size=(0, self.embedding_dim), dtype=torch.float, device=device)

    reduced_edge_index = hyperedge_index_wrapper.reduce(
        self.graph_reduction_strategy,
        num_nodes=num_nodes,
    )
    edge_index_wrapper = EdgeIndex(reduced_edge_index).remove_selfloops()
    if edge_index_wrapper.num_edges == 0:
        warnings.warn(
            """
            Clique expansion produced no non-self-loop edges. Returning zero node features.
            """,
            category=UserWarning,
            stacklevel=2,
        )
        return torch.zeros(
            size=(num_nodes, self.embedding_dim),
            dtype=torch.float,
            device=device,
        )

    edge_index = edge_index_wrapper.item.to(device)
    model = PyGNode2Vec(
        edge_index=edge_index,
        embedding_dim=self.embedding_dim,
        walk_length=self.walk_length,
        context_size=self.context_size,
        walks_per_node=self.num_walks_per_node,
        p=self.p,
        q=self.q,
        num_negative_samples=self.num_negative_samples,
        num_nodes=num_nodes,
        sparse=self.sparse,
    ).to(device)

    data_loader = model.loader(batch_size=self.batch_size, shuffle=True)
    optimizer = (
        optim.SparseAdam(model.parameters(), lr=self.learning_rate)
        if self.sparse
        else optim.Adam(model.parameters(), lr=self.learning_rate)
    )

    if self.verbose:
        print(f"Training Node2Vec model for {self.num_epochs} epochs...")

    model.train()
    for epoch in range(self.num_epochs):
        if self.verbose:
            print(f"Epoch {epoch + 1}/{self.num_epochs}\r", end="")

        # Iterate over batches of positive and negative random walks
        for positive_random_walk, negative_random_walk in data_loader:
            positive_random_walk_on_device = positive_random_walk.to(device)
            negative_random_walk_on_device = negative_random_walk.to(device)

            optimizer.zero_grad()
            loss = model.loss(positive_random_walk_on_device, negative_random_walk_on_device)
            loss.backward()
            optimizer.step()

    if self.verbose:
        print("Training complete. Generating node embeddings...")

    model.eval()
    with torch.no_grad():
        x: Tensor = model()  # shape (num_nodes, num_features)

    # Detach node embeddings from computation graph and return them
    return x.detach().to(device)

__validate()

Validate Node2Vec enrichment configuration.

Raises:

Type Description
ValueError

If any configuration value is outside its supported range.

Source code in hypertorch/data/enricher.py
def __validate(self) -> None:
    """
    Validate Node2Vec enrichment configuration.

    Raises:
        ValueError: If any configuration value is outside its supported range.
    """
    validate_is_positive("num_features", self.embedding_dim)
    validate_is_positive("walk_length", self.walk_length)
    validate_is_positive("context_size", self.context_size)
    if self.walk_length < self.context_size:
        raise ValueError(
            "Expected walk_length >= context_size, got "
            f"walk_length={self.walk_length}, context_size={self.context_size}."
        )

    validate_is_positive("num_walks_per_node", self.num_walks_per_node)
    validate_is_finite("p", self.p)
    validate_is_positive("p", self.p)
    validate_is_finite("q", self.q)
    validate_is_positive("q", self.q)
    validate_is_positive("num_negative_samples", self.num_negative_samples)
    validate_is_non_negative("num_nodes", self.num_nodes)
    validate_is_positive("num_epochs", self.num_epochs)
    validate_is_finite("learning_rate", self.learning_rate)
    validate_is_positive("learning_rate", self.learning_rate)
    validate_is_positive("batch_size", self.batch_size)

NodeEnricher

Bases: Enricher, ABC

Base class for node enrichers.

Source code in hypertorch/data/enricher.py
class NodeEnricher(Enricher, ABC):
    """
    Base class for node enrichers.
    """

    pass

VilLainEnricher

Bases: _VilLainTrainer, NodeEnricher

Enrich node features with VilLain embeddings learned from hypergraph topology.

Source code in hypertorch/data/enricher.py
class VilLainEnricher(_VilLainTrainer, NodeEnricher):
    """
    Enrich node features with VilLain embeddings learned from hypergraph topology.
    """

    def __init__(
        self,
        num_features: int,
        num_nodes: int = 0,
        num_hyperedges: int = 0,
        labels_per_subspace: int = 2,
        training_steps: int = 4,
        generation_steps: int = 100,
        tau: float = 1.0,
        eps: float = 1e-10,
        num_epochs: int = 5,
        learning_rate: float = 0.01,
        weight_decay: float = 0.0,
        cache_dir: str | None = None,
        verbose: bool = False,
    ):
        """
        Initialize the VilLain node feature enricher.

        Args:
            num_features: Dimensionality of the node embeddings to generate.
            num_nodes: Total number of nodes, including isolated nodes missing
                from ``hyperedge_index``.
            num_hyperedges: Total number of hyperedges, including empty hyperedges missing
                from ``hyperedge_index``.
            labels_per_subspace: Number of virtual labels per subspace.
            training_steps: Propagation steps used for VilLain self-supervised loss.
            generation_steps: Propagation steps averaged for final embeddings.
            tau: Gumbel-Softmax temperature.
            eps: Numerical stability constant.
            num_epochs: Number of epochs used to optimize VilLain embeddings.
            learning_rate: Learning rate for embedding optimization.
            weight_decay: Weight decay for the optimizer.
            cache_dir: Optional directory to cache computed features. If ``None``,
                caching is disabled.
            verbose: Whether to print verbose output during training.
        """
        NodeEnricher.__init__(self, cache_dir=cache_dir)
        _VilLainTrainer.__init__(
            self,
            num_features=num_features,
            num_nodes=num_nodes,
            num_hyperedges=num_hyperedges,
            labels_per_subspace=labels_per_subspace,
            training_steps=training_steps,
            generation_steps=generation_steps,
            tau=tau,
            eps=eps,
            num_epochs=num_epochs,
            learning_rate=learning_rate,
            weight_decay=weight_decay,
            verbose=verbose,
        )

    def enrich(self, hyperedge_index: Tensor) -> Tensor:
        """
        Train VilLain on the hypergraph and return node embeddings.

        Args:
            hyperedge_index: Hyperedge index tensor of shape ``(2, num_hyperedges)``.

        Returns:
            node_embeddings: Tensor of shape ``(num_nodes, num_features)`` containing
                VilLain node embeddings.
        """
        num_nodes = self._num_nodes(hyperedge_index)
        if num_nodes == 0:
            warnings.warn(
                "Found no nodes. Returning empty node features.",
                category=UserWarning,
                stacklevel=2,
            )
            return self._empty_features(hyperedge_index)

        model = self._train(hyperedge_index)
        model.eval()
        with torch.no_grad():
            x = model.node_embeddings(
                hyperedge_index=hyperedge_index,
                num_hyperedges=self._num_hyperedges(hyperedge_index),
            )
        return x.detach().to(hyperedge_index.device)

__init__(num_features, num_nodes=0, num_hyperedges=0, labels_per_subspace=2, training_steps=4, generation_steps=100, tau=1.0, eps=1e-10, num_epochs=5, learning_rate=0.01, weight_decay=0.0, cache_dir=None, verbose=False)

Initialize the VilLain node feature enricher.

Parameters:

Name Type Description Default
num_features int

Dimensionality of the node embeddings to generate.

required
num_nodes int

Total number of nodes, including isolated nodes missing from hyperedge_index.

0
num_hyperedges int

Total number of hyperedges, including empty hyperedges missing from hyperedge_index.

0
labels_per_subspace int

Number of virtual labels per subspace.

2
training_steps int

Propagation steps used for VilLain self-supervised loss.

4
generation_steps int

Propagation steps averaged for final embeddings.

100
tau float

Gumbel-Softmax temperature.

1.0
eps float

Numerical stability constant.

1e-10
num_epochs int

Number of epochs used to optimize VilLain embeddings.

5
learning_rate float

Learning rate for embedding optimization.

0.01
weight_decay float

Weight decay for the optimizer.

0.0
cache_dir str | None

Optional directory to cache computed features. If None, caching is disabled.

None
verbose bool

Whether to print verbose output during training.

False
Source code in hypertorch/data/enricher.py
def __init__(
    self,
    num_features: int,
    num_nodes: int = 0,
    num_hyperedges: int = 0,
    labels_per_subspace: int = 2,
    training_steps: int = 4,
    generation_steps: int = 100,
    tau: float = 1.0,
    eps: float = 1e-10,
    num_epochs: int = 5,
    learning_rate: float = 0.01,
    weight_decay: float = 0.0,
    cache_dir: str | None = None,
    verbose: bool = False,
):
    """
    Initialize the VilLain node feature enricher.

    Args:
        num_features: Dimensionality of the node embeddings to generate.
        num_nodes: Total number of nodes, including isolated nodes missing
            from ``hyperedge_index``.
        num_hyperedges: Total number of hyperedges, including empty hyperedges missing
            from ``hyperedge_index``.
        labels_per_subspace: Number of virtual labels per subspace.
        training_steps: Propagation steps used for VilLain self-supervised loss.
        generation_steps: Propagation steps averaged for final embeddings.
        tau: Gumbel-Softmax temperature.
        eps: Numerical stability constant.
        num_epochs: Number of epochs used to optimize VilLain embeddings.
        learning_rate: Learning rate for embedding optimization.
        weight_decay: Weight decay for the optimizer.
        cache_dir: Optional directory to cache computed features. If ``None``,
            caching is disabled.
        verbose: Whether to print verbose output during training.
    """
    NodeEnricher.__init__(self, cache_dir=cache_dir)
    _VilLainTrainer.__init__(
        self,
        num_features=num_features,
        num_nodes=num_nodes,
        num_hyperedges=num_hyperedges,
        labels_per_subspace=labels_per_subspace,
        training_steps=training_steps,
        generation_steps=generation_steps,
        tau=tau,
        eps=eps,
        num_epochs=num_epochs,
        learning_rate=learning_rate,
        weight_decay=weight_decay,
        verbose=verbose,
    )

enrich(hyperedge_index)

Train VilLain on the hypergraph and return node embeddings.

Parameters:

Name Type Description Default
hyperedge_index Tensor

Hyperedge index tensor of shape (2, num_hyperedges).

required

Returns:

Name Type Description
node_embeddings Tensor

Tensor of shape (num_nodes, num_features) containing VilLain node embeddings.

Source code in hypertorch/data/enricher.py
def enrich(self, hyperedge_index: Tensor) -> Tensor:
    """
    Train VilLain on the hypergraph and return node embeddings.

    Args:
        hyperedge_index: Hyperedge index tensor of shape ``(2, num_hyperedges)``.

    Returns:
        node_embeddings: Tensor of shape ``(num_nodes, num_features)`` containing
            VilLain node embeddings.
    """
    num_nodes = self._num_nodes(hyperedge_index)
    if num_nodes == 0:
        warnings.warn(
            "Found no nodes. Returning empty node features.",
            category=UserWarning,
            stacklevel=2,
        )
        return self._empty_features(hyperedge_index)

    model = self._train(hyperedge_index)
    model.eval()
    with torch.no_grad():
        x = model.node_embeddings(
            hyperedge_index=hyperedge_index,
            num_hyperedges=self._num_hyperedges(hyperedge_index),
        )
    return x.detach().to(hyperedge_index.device)

VilLainHyperedgeAttrsEnricher

Bases: _VilLainTrainer, HyperedgeAttrsEnricher

Enrich hyperedge attributes with VilLain embeddings learned from hypergraph topology.

Source code in hypertorch/data/enricher.py
class VilLainHyperedgeAttrsEnricher(_VilLainTrainer, HyperedgeAttrsEnricher):
    """
    Enrich hyperedge attributes with VilLain embeddings learned from hypergraph topology.
    """

    def __init__(
        self,
        num_features: int,
        num_nodes: int = 0,
        num_hyperedges: int = 0,
        labels_per_subspace: int = 2,
        training_steps: int = 4,
        generation_steps: int = 100,
        tau: float = 1.0,
        eps: float = 1e-10,
        num_epochs: int = 5,
        learning_rate: float = 0.01,
        weight_decay: float = 0.0,
        cache_dir: str | None = None,
        verbose: bool = False,
    ):
        """
        Initialize the VilLain hyperedge attribute enricher.

        Args:
            num_features: Dimensionality of the hyperedge embeddings to generate.
            num_nodes: Total number of nodes, including isolated nodes missing from
                ``hyperedge_index``.
            num_hyperedges: Total number of hyperedges, including empty hyperedges missing
                from ``hyperedge_index``.
            labels_per_subspace: Number of virtual labels per subspace.
            training_steps: Propagation steps used for VilLain self-supervised loss.
            generation_steps: Propagation steps averaged for final embeddings.
            tau: Gumbel-Softmax temperature.
            eps: Numerical stability constant.
            num_epochs: Number of epochs used to optimize VilLain embeddings.
            learning_rate: Learning rate for embedding optimization.
            weight_decay: Weight decay for the optimizer.
            cache_dir: Optional directory to cache computed features. If ``None``,
                caching is disabled.
            verbose: Whether to print verbose output during training.
        """
        HyperedgeAttrsEnricher.__init__(self, cache_dir=cache_dir)
        _VilLainTrainer.__init__(
            self,
            num_features=num_features,
            num_nodes=num_nodes,
            num_hyperedges=num_hyperedges,
            labels_per_subspace=labels_per_subspace,
            training_steps=training_steps,
            generation_steps=generation_steps,
            tau=tau,
            eps=eps,
            num_epochs=num_epochs,
            learning_rate=learning_rate,
            weight_decay=weight_decay,
            verbose=verbose,
        )

    def enrich(self, hyperedge_index: Tensor) -> Tensor:
        """
        Train VilLain on the hypergraph and return hyperedge embeddings.

        Args:
            hyperedge_index: Hyperedge index tensor of shape ``(2, num_hyperedges)``.

        Returns:
            hyperedge_embeddings: Tensor of shape ``(num_hyperedges, num_features)``
                containing VilLain hyperedge embeddings.
        """
        num_hyperedges = self._num_hyperedges(hyperedge_index)
        if num_hyperedges == 0:
            warnings.warn(
                "Found no hyperedges. Returning empty hyperedge attributes.",
                category=UserWarning,
                stacklevel=2,
            )
            return self._empty_features(hyperedge_index)

        model = self._train(hyperedge_index)
        model.eval()
        with torch.no_grad():
            hyperedge_attr = model.hyperedge_embeddings(
                hyperedge_index=hyperedge_index,
                num_hyperedges=num_hyperedges,
            )
        return hyperedge_attr.detach().to(hyperedge_index.device)

__init__(num_features, num_nodes=0, num_hyperedges=0, labels_per_subspace=2, training_steps=4, generation_steps=100, tau=1.0, eps=1e-10, num_epochs=5, learning_rate=0.01, weight_decay=0.0, cache_dir=None, verbose=False)

Initialize the VilLain hyperedge attribute enricher.

Parameters:

Name Type Description Default
num_features int

Dimensionality of the hyperedge embeddings to generate.

required
num_nodes int

Total number of nodes, including isolated nodes missing from hyperedge_index.

0
num_hyperedges int

Total number of hyperedges, including empty hyperedges missing from hyperedge_index.

0
labels_per_subspace int

Number of virtual labels per subspace.

2
training_steps int

Propagation steps used for VilLain self-supervised loss.

4
generation_steps int

Propagation steps averaged for final embeddings.

100
tau float

Gumbel-Softmax temperature.

1.0
eps float

Numerical stability constant.

1e-10
num_epochs int

Number of epochs used to optimize VilLain embeddings.

5
learning_rate float

Learning rate for embedding optimization.

0.01
weight_decay float

Weight decay for the optimizer.

0.0
cache_dir str | None

Optional directory to cache computed features. If None, caching is disabled.

None
verbose bool

Whether to print verbose output during training.

False
Source code in hypertorch/data/enricher.py
def __init__(
    self,
    num_features: int,
    num_nodes: int = 0,
    num_hyperedges: int = 0,
    labels_per_subspace: int = 2,
    training_steps: int = 4,
    generation_steps: int = 100,
    tau: float = 1.0,
    eps: float = 1e-10,
    num_epochs: int = 5,
    learning_rate: float = 0.01,
    weight_decay: float = 0.0,
    cache_dir: str | None = None,
    verbose: bool = False,
):
    """
    Initialize the VilLain hyperedge attribute enricher.

    Args:
        num_features: Dimensionality of the hyperedge embeddings to generate.
        num_nodes: Total number of nodes, including isolated nodes missing from
            ``hyperedge_index``.
        num_hyperedges: Total number of hyperedges, including empty hyperedges missing
            from ``hyperedge_index``.
        labels_per_subspace: Number of virtual labels per subspace.
        training_steps: Propagation steps used for VilLain self-supervised loss.
        generation_steps: Propagation steps averaged for final embeddings.
        tau: Gumbel-Softmax temperature.
        eps: Numerical stability constant.
        num_epochs: Number of epochs used to optimize VilLain embeddings.
        learning_rate: Learning rate for embedding optimization.
        weight_decay: Weight decay for the optimizer.
        cache_dir: Optional directory to cache computed features. If ``None``,
            caching is disabled.
        verbose: Whether to print verbose output during training.
    """
    HyperedgeAttrsEnricher.__init__(self, cache_dir=cache_dir)
    _VilLainTrainer.__init__(
        self,
        num_features=num_features,
        num_nodes=num_nodes,
        num_hyperedges=num_hyperedges,
        labels_per_subspace=labels_per_subspace,
        training_steps=training_steps,
        generation_steps=generation_steps,
        tau=tau,
        eps=eps,
        num_epochs=num_epochs,
        learning_rate=learning_rate,
        weight_decay=weight_decay,
        verbose=verbose,
    )

enrich(hyperedge_index)

Train VilLain on the hypergraph and return hyperedge embeddings.

Parameters:

Name Type Description Default
hyperedge_index Tensor

Hyperedge index tensor of shape (2, num_hyperedges).

required

Returns:

Name Type Description
hyperedge_embeddings Tensor

Tensor of shape (num_hyperedges, num_features) containing VilLain hyperedge embeddings.

Source code in hypertorch/data/enricher.py
def enrich(self, hyperedge_index: Tensor) -> Tensor:
    """
    Train VilLain on the hypergraph and return hyperedge embeddings.

    Args:
        hyperedge_index: Hyperedge index tensor of shape ``(2, num_hyperedges)``.

    Returns:
        hyperedge_embeddings: Tensor of shape ``(num_hyperedges, num_features)``
            containing VilLain hyperedge embeddings.
    """
    num_hyperedges = self._num_hyperedges(hyperedge_index)
    if num_hyperedges == 0:
        warnings.warn(
            "Found no hyperedges. Returning empty hyperedge attributes.",
            category=UserWarning,
            stacklevel=2,
        )
        return self._empty_features(hyperedge_index)

    model = self._train(hyperedge_index)
    model.eval()
    with torch.no_grad():
        hyperedge_attr = model.hyperedge_embeddings(
            hyperedge_index=hyperedge_index,
            num_hyperedges=num_hyperedges,
        )
    return hyperedge_attr.detach().to(hyperedge_index.device)

CliqueNegativeSampler

Bases: SameNodeSpaceNegativeSampler

Sample negative hyperedges that are cliques in the underlying graph.

The underlying graph is obtained through clique expansion: two nodes are adjacent when they co-occur in at least one positive hyperedge. A sampled negative hyperedge contains num_nodes_per_sample nodes where every pair is adjacent, and the node set must not already exist as a positive hyperedge.

Attributes:

Name Type Description
num_negative_samples int

Number of negative hyperedges to generate.

num_nodes_per_sample int

Number of nodes per negative hyperedge. Must be at least 2.

max_candidates int | None

Optional upper bound for full-size clique candidates enumerated during search. If None, it means no explicit cap. The limit counts every full-size clique candidate encountered before positive-hyperedge filtering, so positive hyperedges still consume the budget because they still require search work. This is a safety guard for dense graphs where clique enumeration can grow quickly. For example, max_candidates=10_000 means the sampler stops if finding candidates requires enumerating more than 10,000 cliques of size num_nodes_per_sample. It does not control how many negatives are returned, as that is controlled by num_negative_samples.

Source code in hypertorch/data/negative_sampler.py
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
class CliqueNegativeSampler(SameNodeSpaceNegativeSampler):
    """
    Sample negative hyperedges that are cliques in the underlying graph.

    The underlying graph is obtained through clique expansion: two nodes are adjacent when
    they co-occur in at least one positive hyperedge. A sampled negative hyperedge contains
    ``num_nodes_per_sample`` nodes where every pair is adjacent, and the node set must not
    already exist as a positive hyperedge.

    Attributes:
        num_negative_samples: Number of negative hyperedges to generate.
        num_nodes_per_sample: Number of nodes per negative hyperedge. Must be at least 2.
        max_candidates: Optional upper bound for full-size clique candidates enumerated
            during search. If ``None``, it means no explicit cap. The limit counts every full-size
            clique candidate encountered before positive-hyperedge filtering,
            so positive hyperedges still consume the budget because they still require search work.
            This is a safety guard for dense graphs where clique enumeration can grow quickly.
            For example, ``max_candidates=10_000`` means the sampler stops if finding candidates
            requires enumerating more than 10,000 cliques of size ``num_nodes_per_sample``.
            It does not control how many negatives are returned, as that is controlled
            by ``num_negative_samples``.
    """

    def __init__(
        self,
        num_negative_samples: int,
        num_nodes_per_sample: int,
        hyperedge_attr_enricher: HyperedgeAttrsEnricher | None = None,
        hyperedge_weights_enricher: HyperedgeWeightsEnricher | None = None,
        return_0based_negatives: bool = False,
        max_candidates: int | None = None,
    ):
        """
        Initialize the clique negative sampler.

        Args:
            num_negative_samples: Number of negative hyperedges to generate.
            num_nodes_per_sample: Number of nodes per negative hyperedge. Must be at least 2.
            hyperedge_attr_enricher: Optional enricher to generate attributes for
                sampled negatives.
            hyperedge_weights_enricher: Optional enricher to generate weights for
                sampled negatives.
            return_0based_negatives: If ``True``, returned negative node and hyperedge IDs
                are rebased to 0-based IDs.
            max_candidates: Optional upper bound for full-size clique candidates enumerated
                during search. If ``None``, there is no explicit cap.

        Raises:
            ValueError: If numeric arguments are invalid.
        """
        if num_negative_samples <= 0:
            raise ValueError(f"num_negative_samples must be positive, got {num_negative_samples}.")
        if num_nodes_per_sample < 2:
            raise ValueError(
                f"num_nodes_per_sample must be at least 2 for clique "
                f"negative sampling, got {num_nodes_per_sample}."
            )
        if max_candidates is not None and max_candidates <= 0:
            raise ValueError(
                f"max_candidates must be positive when provided, got {max_candidates}."
            )

        super().__init__(
            hyperedge_attr_enricher=hyperedge_attr_enricher,
            hyperedge_weights_enricher=hyperedge_weights_enricher,
            return_0based_negatives=return_0based_negatives,
        )
        self.num_negative_samples: int = num_negative_samples
        self.num_nodes_per_sample: int = num_nodes_per_sample
        self.max_candidates: int | None = max_candidates

    def sample(self, hdata: HData, seed: int | None = None) -> HData:
        """
        Generate clique-based negative hyperedges from the input hypergraph.

        Args:
            hdata: Input hypergraph data.
            seed: Optional random seed for reproducible candidate selection. Defaults to ``None``.

        Returns:
            hdata: A new `HData` instance containing only sampled negative hyperedges.

        Raises:
            ValueError: If too few nodes or valid clique negatives are available.
        """
        if self.num_nodes_per_sample > hdata.num_nodes:
            raise ValueError(
                f"Asked to create samples with {self.num_nodes_per_sample} nodes, "
                f"but only {hdata.num_nodes} nodes are available."
            )
        device = hdata.device

        # Example: hyperedge_index = [[0, 1, 2, 0, 3],
        #                             [0, 0, 0, 1, 1]],
        #          num_nodes_per_sample = 3
        #          -> positive_hyperedge_signatures = {(0, 1, 2)}
        #          so (0, 1, 2) is rejected even though it is a clique.
        positive_hyperedge_signatures = {
            signature
            for signature in self._hyperedges_signatures(hdata.hyperedge_index)
            if len(signature) == self.num_nodes_per_sample
        }

        # First build the clique-expanded graph, then enumerate node sets that are cliques
        # in that graph: these are the negatives with pairwise cohesion.
        # Example: hyperedge_index = [[0, 1, 2, 0, 3],
        #                             [0, 0, 0, 1, 1]],
        #          -> adjacency_list = [0: {1, 2, 3}, 1: {0, 2}, 2: {0, 1}, 3: {0}]
        #          -> clique candidates of size 3 = [(0, 1, 2)]
        #          -> valid_clique_candidates = [] because (0, 1, 2) is already positive
        #             if num_nodes_per_sample == 2 instead:
        #             clique candidates would be [(0, 1), (0, 2), (0, 3), (1, 2)]
        adjacency_list = HyperedgeIndex(hdata.hyperedge_index).get_clique_expansion_adjacency_list(
            num_nodes=hdata.num_nodes
        )
        valid_clique_candidates = self.__find_valid_clique_candidates(
            adjacency_list=adjacency_list,
            positive_hyperedge_signatures=positive_hyperedge_signatures,
        )

        (
            sampled_hyperedge_indexes,
            sampled_hyperedge_attrs,
            sampled_negative_node_ids,
            new_hyperedge_id_offset,
        ) = self.__sample_loop(
            hdata=hdata,
            clique_candidates=valid_clique_candidates,
            seed=seed,
        )

        negative_node_ids_tensor = torch.tensor(
            sorted(sampled_negative_node_ids),
            dtype=torch.long,
            device=device,
        )
        new_x, num_negative_nodes = self._new_x(hdata.x, negative_node_ids_tensor)

        negative_hyperedge_ids = self._new_negative_hyperedge_ids(
            new_hyperedge_id_offset=new_hyperedge_id_offset,
            num_negative_samples=self.num_negative_samples,
            device=device,
        )

        negative_hyperedge_index = self._new_negative_hyperedge_index(
            sampled_hyperedge_indexes,
            negative_node_ids_tensor,
            negative_hyperedge_ids,
        )

        negative_hyperedge_attr = self._new_enriched_hyperedge_attr(
            hyperedge_attr_enricher=self.hyperedge_attr_enricher,
            negative_hyperedge_index=negative_hyperedge_index,
        )
        if negative_hyperedge_attr is None:
            negative_hyperedge_attr = self._new_hyperedge_attr(
                sampled_hyperedge_attrs=sampled_hyperedge_attrs,
                hyperedge_attr=hdata.hyperedge_attr,
            )

        return HData(
            x=new_x,
            hyperedge_index=negative_hyperedge_index,
            hyperedge_weights=self._new_enriched_hyperedge_weights(
                hyperedge_weights_enricher=self.hyperedge_weights_enricher,
                negative_hyperedge_index=negative_hyperedge_index,
            ),
            hyperedge_attr=negative_hyperedge_attr,
            num_nodes=num_negative_nodes,
            num_hyperedges=self.num_negative_samples,
            global_node_ids=self._new_global_node_ids(
                global_node_ids=hdata.global_node_ids,
                negative_node_ids=negative_node_ids_tensor,
            ),
        ).with_y_zeros()

    def __expand_clique_candidates(
        self,
        prefix: tuple[int, ...],
        candidates: list[int],
        adjacency_list: list[set[int]],
        positive_hyperedge_signatures: set[tuple[int, ...]],
        valid_candidates: list[tuple[int, ...]],
        enumerated_candidates: int,
    ) -> int:
        """
        Recursively enumerate clique candidates from a clique-expanded adjacency list.

        Args:
            prefix: Current partial clique, represented as sorted node IDs.
            candidates: Node IDs that may extend ``prefix`` while preserving clique structure.
            adjacency_list: Clique-expanded graph adjacency list.
            positive_hyperedge_signatures: Positive hyperedge node signatures that must not
                be returned as negatives.
            valid_candidates: Output list mutated in place with valid negative clique candidates.
            enumerated_candidates: Number of full-size clique candidates visited so far.

        Returns:
            visited: Updated number of full-size clique candidates visited.

        Raises:
            ValueError: If ``max_candidates`` is set and clique enumeration exceeds it.
        """
        if len(prefix) == self.num_nodes_per_sample:  # Found a full-size clique candidate
            if self.max_candidates is not None and enumerated_candidates >= self.max_candidates:
                raise ValueError(
                    f"Clique negative candidate enumeration exceeded "
                    f"max_candidates={self.max_candidates}."
                )
            enumerated_candidates += 1

            signature = self._hyperedge_nodes_signature(prefix)
            if signature not in positive_hyperedge_signatures:
                valid_candidates.append(signature)

            return enumerated_candidates

        for node_idx, node_id in enumerate(candidates):
            # Keep only future candidates adjacent to the new node. Since previous
            # expansion steps already intersected with earlier prefix nodes, every
            # recursive prefix remains a clique
            # Example: prefix = (0,), candidates = [1, 2, 3],
            #          node_idx = 0, node_id = 1, adjacency_list[1] = {0, 2, 3},
            #          -> next_candidates = [2, 3]
            #             We don't pick 0 because it's already in the prefix
            next_candidates = [
                candidate_node_id
                for candidate_node_id in candidates[node_idx + 1 :]
                if candidate_node_id in adjacency_list[node_id]
            ]

            enumerated_candidates = self.__expand_clique_candidates(
                prefix=(*prefix, node_id),
                candidates=next_candidates,
                adjacency_list=adjacency_list,
                positive_hyperedge_signatures=positive_hyperedge_signatures,
                valid_candidates=valid_candidates,
                enumerated_candidates=enumerated_candidates,
            )

        return enumerated_candidates

    def __find_valid_clique_candidates(
        self,
        adjacency_list: list[set[int]],
        positive_hyperedge_signatures: set[tuple[int, ...]],
    ) -> list[tuple[int, ...]]:
        """
        Find valid clique negative candidates in the clique-expanded graph.

        Args:
            adjacency_list: Clique-expanded graph adjacency list.
            positive_hyperedge_signatures: Positive hyperedge node signatures with
                the requested sample size.

        Returns:
            candidates: Clique node signatures that are not positive hyperedges.

        Raises:
            ValueError: If fewer valid clique negatives exist than requested, or if
                ``max_candidates`` is exceeded during enumeration.
        """
        valid_clique_candidates: list[tuple[int, ...]] = []

        num_nodes = len(adjacency_list)
        # Initial candidates are all nodes, the recursive expansion
        # will narrow down to cliques of the right size
        all_nodes_as_candidates = list(range(num_nodes))
        self.__expand_clique_candidates(
            prefix=(),
            candidates=all_nodes_as_candidates,
            adjacency_list=adjacency_list,
            positive_hyperedge_signatures=positive_hyperedge_signatures,
            valid_candidates=valid_clique_candidates,
            enumerated_candidates=0,
        )

        if len(valid_clique_candidates) < self.num_negative_samples:
            raise ValueError(
                "Asked to create "
                f"{self.num_negative_samples} clique negative samples with "
                f"{self.num_nodes_per_sample} nodes each, but only "
                f"{len(valid_clique_candidates)} are available."
            )

        return valid_clique_candidates

    def __sample_loop(
        self,
        hdata: HData,
        clique_candidates: list[tuple[int, ...]],
        seed: int | None = None,
    ) -> tuple[list[Tensor], list[Tensor], set[int], int]:
        """
        Sample from valid clique candidates and build negative hyperedge tensors.

        Args:
            hdata: Input hypergraph data used for feature, attribute, and ID context.
            clique_candidates: Valid clique negative candidates to sample from.
            seed: Optional seed for reproducible candidate shuffling and random attributes.

        Returns:
            sampled_hyperedge_indexes:  sampled hyperedge index tensors
            sampled_hyperedge_attrs: sampled hyperedge attribute tensors.
            sampled_negative_node_ids: sampled negative node IDs.
            new_hyperedge_id_offset: first negative hyperedge ID.
        """
        device = hdata.device
        generator = create_seeded_torch_generator(device=device, seed=seed)

        # Shuffle the clique candidates with the optional generator
        # Example: clique_candidates = [(0, 1, 3),   # index 0
        #                               (0, 2, 3),   # index 1
        #                               (1, 2, 3)],  # index 2
        #          -> shuffled_clique_candidate_indexes = [2, 0, 1]
        #          as we only need 2 samples
        #          -> sampled_clique_candidate_indexes = [2, 0] if num_negative_samples=2
        #          -> sampled_clique_candidates = [(1, 2, 3),  # index 2 in clique_candidates
        #                                          (0, 1, 3)]  # index 0 in clique_candidates
        num_valid_clique_candidates = len(clique_candidates)
        shuffled_clique_candidate_indexes = torch.randperm(
            n=num_valid_clique_candidates,
            generator=generator,
            dtype=torch.long,
            device=device,
        )
        sampled_clique_candidate_indexes = shuffled_clique_candidate_indexes[
            : self.num_negative_samples
        ]
        sampled_clique_candidates = [
            clique_candidates[int(clique_candidate_idx)]
            for clique_candidate_idx in sampled_clique_candidate_indexes.tolist()
        ]

        sampled_negative_node_ids: set[int] = set()
        sampled_hyperedge_indexes: list[Tensor] = []
        sampled_hyperedge_attrs: list[Tensor] = []
        new_hyperedge_id_offset = hdata.num_hyperedges

        for new_hyperedge_id, sampled_clique_candidate in enumerate(sampled_clique_candidates):
            # Example: new_hyperedge_id_offset = 5, new_hyperedge_id = 0,
            #          sampled_candidate = (0, 2, 3)
            #          -> sampled_node_ids = [0, 2, 3]
            #          -> sampled_hyperedge_id_tensor = [5, 5, 5]
            #          -> sampled_hyperedge_index = [[0, 2, 3],
            #                                        [5, 5, 5]]
            sampled_node_ids = torch.tensor(
                sampled_clique_candidate, dtype=torch.long, device=device
            )
            sampled_hyperedge_id_tensor = torch.full(
                size=(self.num_nodes_per_sample,),
                fill_value=new_hyperedge_id + new_hyperedge_id_offset,
                dtype=torch.long,
                device=device,
            )
            sampled_hyperedge_indexes.append(
                torch.stack([sampled_node_ids, sampled_hyperedge_id_tensor], dim=0)
            )
            sampled_negative_node_ids.update(sampled_clique_candidate)

            if hdata.hyperedge_attr is not None:
                random_hyperedge_attr = torch.randn(
                    size=hdata.hyperedge_attr[0].shape,
                    dtype=hdata.hyperedge_attr.dtype,
                    generator=generator,
                    device=device,
                )
                sampled_hyperedge_attrs.append(random_hyperedge_attr)

        return (
            sampled_hyperedge_indexes,
            sampled_hyperedge_attrs,
            sampled_negative_node_ids,
            new_hyperedge_id_offset,
        )

__init__(num_negative_samples, num_nodes_per_sample, hyperedge_attr_enricher=None, hyperedge_weights_enricher=None, return_0based_negatives=False, max_candidates=None)

Initialize the clique negative sampler.

Parameters:

Name Type Description Default
num_negative_samples int

Number of negative hyperedges to generate.

required
num_nodes_per_sample int

Number of nodes per negative hyperedge. Must be at least 2.

required
hyperedge_attr_enricher HyperedgeAttrsEnricher | None

Optional enricher to generate attributes for sampled negatives.

None
hyperedge_weights_enricher HyperedgeWeightsEnricher | None

Optional enricher to generate weights for sampled negatives.

None
return_0based_negatives bool

If True, returned negative node and hyperedge IDs are rebased to 0-based IDs.

False
max_candidates int | None

Optional upper bound for full-size clique candidates enumerated during search. If None, there is no explicit cap.

None

Raises:

Type Description
ValueError

If numeric arguments are invalid.

Source code in hypertorch/data/negative_sampler.py
def __init__(
    self,
    num_negative_samples: int,
    num_nodes_per_sample: int,
    hyperedge_attr_enricher: HyperedgeAttrsEnricher | None = None,
    hyperedge_weights_enricher: HyperedgeWeightsEnricher | None = None,
    return_0based_negatives: bool = False,
    max_candidates: int | None = None,
):
    """
    Initialize the clique negative sampler.

    Args:
        num_negative_samples: Number of negative hyperedges to generate.
        num_nodes_per_sample: Number of nodes per negative hyperedge. Must be at least 2.
        hyperedge_attr_enricher: Optional enricher to generate attributes for
            sampled negatives.
        hyperedge_weights_enricher: Optional enricher to generate weights for
            sampled negatives.
        return_0based_negatives: If ``True``, returned negative node and hyperedge IDs
            are rebased to 0-based IDs.
        max_candidates: Optional upper bound for full-size clique candidates enumerated
            during search. If ``None``, there is no explicit cap.

    Raises:
        ValueError: If numeric arguments are invalid.
    """
    if num_negative_samples <= 0:
        raise ValueError(f"num_negative_samples must be positive, got {num_negative_samples}.")
    if num_nodes_per_sample < 2:
        raise ValueError(
            f"num_nodes_per_sample must be at least 2 for clique "
            f"negative sampling, got {num_nodes_per_sample}."
        )
    if max_candidates is not None and max_candidates <= 0:
        raise ValueError(
            f"max_candidates must be positive when provided, got {max_candidates}."
        )

    super().__init__(
        hyperedge_attr_enricher=hyperedge_attr_enricher,
        hyperedge_weights_enricher=hyperedge_weights_enricher,
        return_0based_negatives=return_0based_negatives,
    )
    self.num_negative_samples: int = num_negative_samples
    self.num_nodes_per_sample: int = num_nodes_per_sample
    self.max_candidates: int | None = max_candidates

sample(hdata, seed=None)

Generate clique-based negative hyperedges from the input hypergraph.

Parameters:

Name Type Description Default
hdata HData

Input hypergraph data.

required
seed int | None

Optional random seed for reproducible candidate selection. Defaults to None.

None

Returns:

Name Type Description
hdata HData

A new HData instance containing only sampled negative hyperedges.

Raises:

Type Description
ValueError

If too few nodes or valid clique negatives are available.

Source code in hypertorch/data/negative_sampler.py
def sample(self, hdata: HData, seed: int | None = None) -> HData:
    """
    Generate clique-based negative hyperedges from the input hypergraph.

    Args:
        hdata: Input hypergraph data.
        seed: Optional random seed for reproducible candidate selection. Defaults to ``None``.

    Returns:
        hdata: A new `HData` instance containing only sampled negative hyperedges.

    Raises:
        ValueError: If too few nodes or valid clique negatives are available.
    """
    if self.num_nodes_per_sample > hdata.num_nodes:
        raise ValueError(
            f"Asked to create samples with {self.num_nodes_per_sample} nodes, "
            f"but only {hdata.num_nodes} nodes are available."
        )
    device = hdata.device

    # Example: hyperedge_index = [[0, 1, 2, 0, 3],
    #                             [0, 0, 0, 1, 1]],
    #          num_nodes_per_sample = 3
    #          -> positive_hyperedge_signatures = {(0, 1, 2)}
    #          so (0, 1, 2) is rejected even though it is a clique.
    positive_hyperedge_signatures = {
        signature
        for signature in self._hyperedges_signatures(hdata.hyperedge_index)
        if len(signature) == self.num_nodes_per_sample
    }

    # First build the clique-expanded graph, then enumerate node sets that are cliques
    # in that graph: these are the negatives with pairwise cohesion.
    # Example: hyperedge_index = [[0, 1, 2, 0, 3],
    #                             [0, 0, 0, 1, 1]],
    #          -> adjacency_list = [0: {1, 2, 3}, 1: {0, 2}, 2: {0, 1}, 3: {0}]
    #          -> clique candidates of size 3 = [(0, 1, 2)]
    #          -> valid_clique_candidates = [] because (0, 1, 2) is already positive
    #             if num_nodes_per_sample == 2 instead:
    #             clique candidates would be [(0, 1), (0, 2), (0, 3), (1, 2)]
    adjacency_list = HyperedgeIndex(hdata.hyperedge_index).get_clique_expansion_adjacency_list(
        num_nodes=hdata.num_nodes
    )
    valid_clique_candidates = self.__find_valid_clique_candidates(
        adjacency_list=adjacency_list,
        positive_hyperedge_signatures=positive_hyperedge_signatures,
    )

    (
        sampled_hyperedge_indexes,
        sampled_hyperedge_attrs,
        sampled_negative_node_ids,
        new_hyperedge_id_offset,
    ) = self.__sample_loop(
        hdata=hdata,
        clique_candidates=valid_clique_candidates,
        seed=seed,
    )

    negative_node_ids_tensor = torch.tensor(
        sorted(sampled_negative_node_ids),
        dtype=torch.long,
        device=device,
    )
    new_x, num_negative_nodes = self._new_x(hdata.x, negative_node_ids_tensor)

    negative_hyperedge_ids = self._new_negative_hyperedge_ids(
        new_hyperedge_id_offset=new_hyperedge_id_offset,
        num_negative_samples=self.num_negative_samples,
        device=device,
    )

    negative_hyperedge_index = self._new_negative_hyperedge_index(
        sampled_hyperedge_indexes,
        negative_node_ids_tensor,
        negative_hyperedge_ids,
    )

    negative_hyperedge_attr = self._new_enriched_hyperedge_attr(
        hyperedge_attr_enricher=self.hyperedge_attr_enricher,
        negative_hyperedge_index=negative_hyperedge_index,
    )
    if negative_hyperedge_attr is None:
        negative_hyperedge_attr = self._new_hyperedge_attr(
            sampled_hyperedge_attrs=sampled_hyperedge_attrs,
            hyperedge_attr=hdata.hyperedge_attr,
        )

    return HData(
        x=new_x,
        hyperedge_index=negative_hyperedge_index,
        hyperedge_weights=self._new_enriched_hyperedge_weights(
            hyperedge_weights_enricher=self.hyperedge_weights_enricher,
            negative_hyperedge_index=negative_hyperedge_index,
        ),
        hyperedge_attr=negative_hyperedge_attr,
        num_nodes=num_negative_nodes,
        num_hyperedges=self.num_negative_samples,
        global_node_ids=self._new_global_node_ids(
            global_node_ids=hdata.global_node_ids,
            negative_node_ids=negative_node_ids_tensor,
        ),
    ).with_y_zeros()

__expand_clique_candidates(prefix, candidates, adjacency_list, positive_hyperedge_signatures, valid_candidates, enumerated_candidates)

Recursively enumerate clique candidates from a clique-expanded adjacency list.

Parameters:

Name Type Description Default
prefix tuple[int, ...]

Current partial clique, represented as sorted node IDs.

required
candidates list[int]

Node IDs that may extend prefix while preserving clique structure.

required
adjacency_list list[set[int]]

Clique-expanded graph adjacency list.

required
positive_hyperedge_signatures set[tuple[int, ...]]

Positive hyperedge node signatures that must not be returned as negatives.

required
valid_candidates list[tuple[int, ...]]

Output list mutated in place with valid negative clique candidates.

required
enumerated_candidates int

Number of full-size clique candidates visited so far.

required

Returns:

Name Type Description
visited int

Updated number of full-size clique candidates visited.

Raises:

Type Description
ValueError

If max_candidates is set and clique enumeration exceeds it.

Source code in hypertorch/data/negative_sampler.py
def __expand_clique_candidates(
    self,
    prefix: tuple[int, ...],
    candidates: list[int],
    adjacency_list: list[set[int]],
    positive_hyperedge_signatures: set[tuple[int, ...]],
    valid_candidates: list[tuple[int, ...]],
    enumerated_candidates: int,
) -> int:
    """
    Recursively enumerate clique candidates from a clique-expanded adjacency list.

    Args:
        prefix: Current partial clique, represented as sorted node IDs.
        candidates: Node IDs that may extend ``prefix`` while preserving clique structure.
        adjacency_list: Clique-expanded graph adjacency list.
        positive_hyperedge_signatures: Positive hyperedge node signatures that must not
            be returned as negatives.
        valid_candidates: Output list mutated in place with valid negative clique candidates.
        enumerated_candidates: Number of full-size clique candidates visited so far.

    Returns:
        visited: Updated number of full-size clique candidates visited.

    Raises:
        ValueError: If ``max_candidates`` is set and clique enumeration exceeds it.
    """
    if len(prefix) == self.num_nodes_per_sample:  # Found a full-size clique candidate
        if self.max_candidates is not None and enumerated_candidates >= self.max_candidates:
            raise ValueError(
                f"Clique negative candidate enumeration exceeded "
                f"max_candidates={self.max_candidates}."
            )
        enumerated_candidates += 1

        signature = self._hyperedge_nodes_signature(prefix)
        if signature not in positive_hyperedge_signatures:
            valid_candidates.append(signature)

        return enumerated_candidates

    for node_idx, node_id in enumerate(candidates):
        # Keep only future candidates adjacent to the new node. Since previous
        # expansion steps already intersected with earlier prefix nodes, every
        # recursive prefix remains a clique
        # Example: prefix = (0,), candidates = [1, 2, 3],
        #          node_idx = 0, node_id = 1, adjacency_list[1] = {0, 2, 3},
        #          -> next_candidates = [2, 3]
        #             We don't pick 0 because it's already in the prefix
        next_candidates = [
            candidate_node_id
            for candidate_node_id in candidates[node_idx + 1 :]
            if candidate_node_id in adjacency_list[node_id]
        ]

        enumerated_candidates = self.__expand_clique_candidates(
            prefix=(*prefix, node_id),
            candidates=next_candidates,
            adjacency_list=adjacency_list,
            positive_hyperedge_signatures=positive_hyperedge_signatures,
            valid_candidates=valid_candidates,
            enumerated_candidates=enumerated_candidates,
        )

    return enumerated_candidates

__find_valid_clique_candidates(adjacency_list, positive_hyperedge_signatures)

Find valid clique negative candidates in the clique-expanded graph.

Parameters:

Name Type Description Default
adjacency_list list[set[int]]

Clique-expanded graph adjacency list.

required
positive_hyperedge_signatures set[tuple[int, ...]]

Positive hyperedge node signatures with the requested sample size.

required

Returns:

Name Type Description
candidates list[tuple[int, ...]]

Clique node signatures that are not positive hyperedges.

Raises:

Type Description
ValueError

If fewer valid clique negatives exist than requested, or if max_candidates is exceeded during enumeration.

Source code in hypertorch/data/negative_sampler.py
def __find_valid_clique_candidates(
    self,
    adjacency_list: list[set[int]],
    positive_hyperedge_signatures: set[tuple[int, ...]],
) -> list[tuple[int, ...]]:
    """
    Find valid clique negative candidates in the clique-expanded graph.

    Args:
        adjacency_list: Clique-expanded graph adjacency list.
        positive_hyperedge_signatures: Positive hyperedge node signatures with
            the requested sample size.

    Returns:
        candidates: Clique node signatures that are not positive hyperedges.

    Raises:
        ValueError: If fewer valid clique negatives exist than requested, or if
            ``max_candidates`` is exceeded during enumeration.
    """
    valid_clique_candidates: list[tuple[int, ...]] = []

    num_nodes = len(adjacency_list)
    # Initial candidates are all nodes, the recursive expansion
    # will narrow down to cliques of the right size
    all_nodes_as_candidates = list(range(num_nodes))
    self.__expand_clique_candidates(
        prefix=(),
        candidates=all_nodes_as_candidates,
        adjacency_list=adjacency_list,
        positive_hyperedge_signatures=positive_hyperedge_signatures,
        valid_candidates=valid_clique_candidates,
        enumerated_candidates=0,
    )

    if len(valid_clique_candidates) < self.num_negative_samples:
        raise ValueError(
            "Asked to create "
            f"{self.num_negative_samples} clique negative samples with "
            f"{self.num_nodes_per_sample} nodes each, but only "
            f"{len(valid_clique_candidates)} are available."
        )

    return valid_clique_candidates

__sample_loop(hdata, clique_candidates, seed=None)

Sample from valid clique candidates and build negative hyperedge tensors.

Parameters:

Name Type Description Default
hdata HData

Input hypergraph data used for feature, attribute, and ID context.

required
clique_candidates list[tuple[int, ...]]

Valid clique negative candidates to sample from.

required
seed int | None

Optional seed for reproducible candidate shuffling and random attributes.

None

Returns:

Name Type Description
sampled_hyperedge_indexes list[Tensor]

sampled hyperedge index tensors

sampled_hyperedge_attrs list[Tensor]

sampled hyperedge attribute tensors.

sampled_negative_node_ids set[int]

sampled negative node IDs.

new_hyperedge_id_offset int

first negative hyperedge ID.

Source code in hypertorch/data/negative_sampler.py
def __sample_loop(
    self,
    hdata: HData,
    clique_candidates: list[tuple[int, ...]],
    seed: int | None = None,
) -> tuple[list[Tensor], list[Tensor], set[int], int]:
    """
    Sample from valid clique candidates and build negative hyperedge tensors.

    Args:
        hdata: Input hypergraph data used for feature, attribute, and ID context.
        clique_candidates: Valid clique negative candidates to sample from.
        seed: Optional seed for reproducible candidate shuffling and random attributes.

    Returns:
        sampled_hyperedge_indexes:  sampled hyperedge index tensors
        sampled_hyperedge_attrs: sampled hyperedge attribute tensors.
        sampled_negative_node_ids: sampled negative node IDs.
        new_hyperedge_id_offset: first negative hyperedge ID.
    """
    device = hdata.device
    generator = create_seeded_torch_generator(device=device, seed=seed)

    # Shuffle the clique candidates with the optional generator
    # Example: clique_candidates = [(0, 1, 3),   # index 0
    #                               (0, 2, 3),   # index 1
    #                               (1, 2, 3)],  # index 2
    #          -> shuffled_clique_candidate_indexes = [2, 0, 1]
    #          as we only need 2 samples
    #          -> sampled_clique_candidate_indexes = [2, 0] if num_negative_samples=2
    #          -> sampled_clique_candidates = [(1, 2, 3),  # index 2 in clique_candidates
    #                                          (0, 1, 3)]  # index 0 in clique_candidates
    num_valid_clique_candidates = len(clique_candidates)
    shuffled_clique_candidate_indexes = torch.randperm(
        n=num_valid_clique_candidates,
        generator=generator,
        dtype=torch.long,
        device=device,
    )
    sampled_clique_candidate_indexes = shuffled_clique_candidate_indexes[
        : self.num_negative_samples
    ]
    sampled_clique_candidates = [
        clique_candidates[int(clique_candidate_idx)]
        for clique_candidate_idx in sampled_clique_candidate_indexes.tolist()
    ]

    sampled_negative_node_ids: set[int] = set()
    sampled_hyperedge_indexes: list[Tensor] = []
    sampled_hyperedge_attrs: list[Tensor] = []
    new_hyperedge_id_offset = hdata.num_hyperedges

    for new_hyperedge_id, sampled_clique_candidate in enumerate(sampled_clique_candidates):
        # Example: new_hyperedge_id_offset = 5, new_hyperedge_id = 0,
        #          sampled_candidate = (0, 2, 3)
        #          -> sampled_node_ids = [0, 2, 3]
        #          -> sampled_hyperedge_id_tensor = [5, 5, 5]
        #          -> sampled_hyperedge_index = [[0, 2, 3],
        #                                        [5, 5, 5]]
        sampled_node_ids = torch.tensor(
            sampled_clique_candidate, dtype=torch.long, device=device
        )
        sampled_hyperedge_id_tensor = torch.full(
            size=(self.num_nodes_per_sample,),
            fill_value=new_hyperedge_id + new_hyperedge_id_offset,
            dtype=torch.long,
            device=device,
        )
        sampled_hyperedge_indexes.append(
            torch.stack([sampled_node_ids, sampled_hyperedge_id_tensor], dim=0)
        )
        sampled_negative_node_ids.update(sampled_clique_candidate)

        if hdata.hyperedge_attr is not None:
            random_hyperedge_attr = torch.randn(
                size=hdata.hyperedge_attr[0].shape,
                dtype=hdata.hyperedge_attr.dtype,
                generator=generator,
                device=device,
            )
            sampled_hyperedge_attrs.append(random_hyperedge_attr)

    return (
        sampled_hyperedge_indexes,
        sampled_hyperedge_attrs,
        sampled_negative_node_ids,
        new_hyperedge_id_offset,
    )

GeneratedNodesNegativeSampler

Bases: NegativeSampler, ABC

Base class for negative samplers that generate new nodes instead of sampling from existing ones.

Attributes:

Name Type Description
node_feature_enricher NodeEnricher

Enricher used to generate features for new nodes.

hyperedge_attr_enricher HyperedgeAttrsEnricher | None

Optional enricher used to generate attributes for sampled hyperedges.

hyperedge_weights_enricher HyperedgeWeightsEnricher | None

Optional enricher used to generate weights for sampled hyperedges.

Source code in hypertorch/data/negative_sampler.py
class GeneratedNodesNegativeSampler(NegativeSampler, ABC):
    """
    Base class for negative samplers that generate new nodes instead of sampling from existing ones.

    Attributes:
        node_feature_enricher: Enricher used to generate features for new nodes.
        hyperedge_attr_enricher: Optional enricher used to generate attributes for
            sampled hyperedges.
        hyperedge_weights_enricher: Optional enricher used to generate weights for
            sampled hyperedges.
    """

    def __init__(
        self,
        node_feature_enricher: NodeEnricher,
        hyperedge_attr_enricher: HyperedgeAttrsEnricher | None = None,
        hyperedge_weights_enricher: HyperedgeWeightsEnricher | None = None,
        return_0based_negatives: bool = False,
    ):
        """
        Initialize the generated-node negative sampler.

        Args:
            node_feature_enricher: Enricher used to generate features for new nodes.
            hyperedge_attr_enricher: Optional enricher used to generate attributes for
                sampled hyperedges.
            hyperedge_weights_enricher: Optional enricher used to generate weights for
                sampled hyperedges.
            return_0based_negatives: If ``True``, returned negatives are rebased to 0-based IDs.
                If ``False``, they retain the original global IDs.
        """
        super().__init__(return_0based_negatives=return_0based_negatives)
        self.node_feature_enricher: NodeEnricher = node_feature_enricher
        self.hyperedge_attr_enricher: HyperedgeAttrsEnricher | None = hyperedge_attr_enricher
        self.hyperedge_weights_enricher: HyperedgeWeightsEnricher | None = (
            hyperedge_weights_enricher
        )

__init__(node_feature_enricher, hyperedge_attr_enricher=None, hyperedge_weights_enricher=None, return_0based_negatives=False)

Initialize the generated-node negative sampler.

Parameters:

Name Type Description Default
node_feature_enricher NodeEnricher

Enricher used to generate features for new nodes.

required
hyperedge_attr_enricher HyperedgeAttrsEnricher | None

Optional enricher used to generate attributes for sampled hyperedges.

None
hyperedge_weights_enricher HyperedgeWeightsEnricher | None

Optional enricher used to generate weights for sampled hyperedges.

None
return_0based_negatives bool

If True, returned negatives are rebased to 0-based IDs. If False, they retain the original global IDs.

False
Source code in hypertorch/data/negative_sampler.py
def __init__(
    self,
    node_feature_enricher: NodeEnricher,
    hyperedge_attr_enricher: HyperedgeAttrsEnricher | None = None,
    hyperedge_weights_enricher: HyperedgeWeightsEnricher | None = None,
    return_0based_negatives: bool = False,
):
    """
    Initialize the generated-node negative sampler.

    Args:
        node_feature_enricher: Enricher used to generate features for new nodes.
        hyperedge_attr_enricher: Optional enricher used to generate attributes for
            sampled hyperedges.
        hyperedge_weights_enricher: Optional enricher used to generate weights for
            sampled hyperedges.
        return_0based_negatives: If ``True``, returned negatives are rebased to 0-based IDs.
            If ``False``, they retain the original global IDs.
    """
    super().__init__(return_0based_negatives=return_0based_negatives)
    self.node_feature_enricher: NodeEnricher = node_feature_enricher
    self.hyperedge_attr_enricher: HyperedgeAttrsEnricher | None = hyperedge_attr_enricher
    self.hyperedge_weights_enricher: HyperedgeWeightsEnricher | None = (
        hyperedge_weights_enricher
    )

NegativeSampler

Bases: ABC

Abstract base class for negative samplers.

Attributes:

Name Type Description
return_0based_negatives bool
  • If True, the negative samples returned by the sample method will have 0-based node and hyperedge IDs.
  • If False, the negative samples will retain the original global node and hyperedge IDs from the input data.
Source code in hypertorch/data/negative_sampler.py
class NegativeSampler(ABC):
    """
    Abstract base class for negative samplers.

    Attributes:
        return_0based_negatives:
            - If ``True``, the negative samples returned by the ``sample`` method
                will have 0-based node and hyperedge IDs.
            - If ``False``, the negative samples will retain the original global node
                and hyperedge IDs from the input data.
    """

    def __init__(self, return_0based_negatives: bool = False):
        """
        Initialize the negative sampler.

        Args:
            return_0based_negatives: If ``True``, returned negative samples have 0-based node
                and hyperedge IDs. If ``False``, they retain the original global IDs.
        """
        super().__init__()
        self.return_0based_negatives: bool = return_0based_negatives

    @abstractmethod
    def sample(self, hdata: HData, seed: int | None = None) -> HData:
        """
        Abstract method for negative sampling.

        Args:
            hdata: The input data object containing graph or hypergraph information.
            seed: Optional random seed for reproducible negative sampling. Defaults to ``None``.

        Returns:
            hdata: The negative samples as a new `HData` object.

        Raises:
            NotImplementedError: If the method is not implemented in a subclass.
        """
        raise NotImplementedError("Subclasses must implement this method.")

    def _new_negative_hyperedge_index(
        self,
        sampled_hyperedge_indexes: list[Tensor],
        negative_node_ids: Tensor,
        negative_hyperedge_ids: Tensor,
    ) -> Tensor:
        """
        Concatenate, sort, and remap the sampled hyperedge indexes for negative samples.

        Args:
            sampled_hyperedge_indexes: List of hyperedge index tensors for each negative sample.
            negative_node_ids: Tensor of negative node IDs.
            negative_hyperedge_ids: Tensor of negative hyperedge IDs.

        Returns:
            hyperedge_index: The concatenated, sorted, and remapped hyperedge index tensor.
            If ``self.return_0based_negatives`` is ``True``, the returned tensor will
                have 0-based node and hyperedge IDs.
            Otherwise, it will retain the original global node and hyperedge IDs
                from the input data.
        """
        negative_hyperedge_index = torch.cat(sampled_hyperedge_indexes, dim=1)
        if not self.return_0based_negatives:
            return negative_hyperedge_index

        negative_hyperedge_index_wrapper = HyperedgeIndex(negative_hyperedge_index).to_0based(
            node_ids_to_rebase=negative_node_ids,
            hyperedge_ids_to_rebase=negative_hyperedge_ids,
        )

        return negative_hyperedge_index_wrapper.item

    def _new_global_node_ids(
        self,
        global_node_ids: Tensor | None,
        negative_node_ids: Tensor,
    ) -> Tensor | None:
        """
        Get the global node IDs for the negative samples.

        Args:
            global_node_ids: The original global node IDs from the input data.
            negative_node_ids: Tensor of negative node IDs.

        Returns:
            global_node_ids: The global node IDs for the negative samples, or ``None`` if
                the input global node IDs are ``None``.
        """
        if global_node_ids is None:
            return None
        return global_node_ids[negative_node_ids]

    def _new_hyperedge_attr(
        self,
        sampled_hyperedge_attrs: list[Tensor],
        hyperedge_attr: Tensor | None = None,
    ) -> Tensor | None:
        """
        Concatenate the hyperedge attributes for the negative samples.

        Args:
            sampled_hyperedge_attrs: List of hyperedge attribute tensors for each negative sample.
            hyperedge_attr: The original hyperedge attributes from the input data.

        Returns:
            hyperedge_attr: The concatenated hyperedge attribute tensor for the negative samples.
        """
        if hyperedge_attr is None or len(sampled_hyperedge_attrs) < 1:
            return None

        negative_hyperedge_attr = torch.stack(sampled_hyperedge_attrs, dim=0)
        return negative_hyperedge_attr

    def _new_enriched_hyperedge_attr(
        self,
        hyperedge_attr_enricher: HyperedgeAttrsEnricher | None,
        negative_hyperedge_index: Tensor,
    ) -> Tensor | None:
        """
        Generate enriched hyperedge attributes for the negative samples.

        Args:
            hyperedge_attr_enricher: An optional `HyperedgeAttrsEnricher` to generate attributes
                for the new hyperedges.
            negative_hyperedge_index: The index tensor for the negative hyperedges.

        Returns:
            hyperedge_attr: The enriched hyperedge attribute tensor for the negative samples,
                or ``None`` if the enricher is not provided.
        """
        if hyperedge_attr_enricher is None:
            return None

        negative_hyperedge_index_0based = (
            HyperedgeIndex(negative_hyperedge_index.clone()).to_0based().item
        )
        return hyperedge_attr_enricher.enrich(negative_hyperedge_index_0based)

    def _new_enriched_hyperedge_weights(
        self,
        hyperedge_weights_enricher: HyperedgeWeightsEnricher | None,
        negative_hyperedge_index: Tensor,
    ) -> Tensor | None:
        """
        Generate enriched hyperedge weights for the negative samples.

        Args:
            hyperedge_weights_enricher: An optional `HyperedgeWeightsEnricher` to
                generate weights for the new hyperedges.
            negative_hyperedge_index: The index tensor for the negative hyperedges.

        Returns:
            hyperedge_weights: The enriched hyperedge weight tensor for the negative samples,
                or ``None`` if the enricher is not provided.
        """
        if hyperedge_weights_enricher is None:
            return None

        negative_hyperedge_index_0based = (
            HyperedgeIndex(negative_hyperedge_index.clone()).to_0based().item
        )
        return hyperedge_weights_enricher.enrich(negative_hyperedge_index_0based)

    def _new_x(self, x: Tensor, negative_node_ids: Tensor) -> tuple[Tensor, int]:
        """
        Get the node feature matrix for the negative samples.

        Args:
            x: The original node feature matrix from the input data.
            negative_node_ids: Tensor of negative node IDs.

        Returns:
            x: The node feature matrix for the negative samples.
            num_negative_nodes: The number of negative nodes.
        """
        return x[negative_node_ids], len(negative_node_ids)

    def _new_negative_hyperedge_ids(
        self,
        new_hyperedge_id_offset: int,
        num_negative_samples: int,
        device: torch.device,
    ) -> Tensor:
        """
        Build the hyperedge IDs assigned to sampled negative hyperedges.

        Args:
            new_hyperedge_id_offset: First negative hyperedge ID,
                usually the number of positive hyperedges in the input data.
            num_negative_samples: Number of negative hyperedge IDs to create.
            device: Device where the returned tensor should be allocated.

        Returns:
            hyperedge_ids: A tensor containing consecutive negative hyperedge IDs.
        """
        # Example: new_hyperedge_id_offset = 3 (if hdata.num_hyperedges was 3)
        #          num_negative_samples = 2
        #          -> negative_hyperedge_ids = [3, 4]
        num_hyperedges_including_negatives = new_hyperedge_id_offset + num_negative_samples
        return torch.arange(
            start=new_hyperedge_id_offset,
            end=num_hyperedges_including_negatives,
            dtype=torch.long,
            device=device,
        )

    def _hyperedges_signatures(self, hyperedge_index: Tensor) -> set[tuple[int, ...]]:
        """
        Build order-independent node signatures for every hyperedge in a hyperedge index.

        Args:
            hyperedge_index: Tensor of shape ``[2, num_incidences]`` containing node
                and hyperedge IDs.

        Returns:
            signatures: A set of sorted node ID tuples, one tuple per hyperedge.
        """
        all_hyperedge_ids = hyperedge_index[1]
        unique_hyperedge_ids = all_hyperedge_ids.unique().tolist()

        signatures: set[tuple[int, ...]] = set()
        for hyperedge_id in unique_hyperedge_ids:
            node_ids_in_hyperedge_mask = all_hyperedge_ids == hyperedge_id
            node_ids_in_hyperedge = hyperedge_index[0][node_ids_in_hyperedge_mask]
            signatures.add(self._hyperedge_nodes_signature(node_ids_in_hyperedge.unique()))
        return signatures

    def _hyperedge_nodes_signature(
        self,
        node_ids: Tensor | list[int] | tuple[int, ...],
    ) -> tuple[int, ...]:
        """
        Convert node IDs into a sorted tuple that can be used as a hyperedge signature.

        Args:
            node_ids: A tensor or sequence containing node IDs for one hyperedge.

        Returns:
            signature: A sorted tuple of Python ``int`` values.
        """
        if isinstance(node_ids, Tensor):
            return tuple(sorted(int(node_id) for node_id in node_ids.tolist()))
        return tuple(sorted(int(node_id) for node_id in node_ids))

__init__(return_0based_negatives=False)

Initialize the negative sampler.

Parameters:

Name Type Description Default
return_0based_negatives bool

If True, returned negative samples have 0-based node and hyperedge IDs. If False, they retain the original global IDs.

False
Source code in hypertorch/data/negative_sampler.py
def __init__(self, return_0based_negatives: bool = False):
    """
    Initialize the negative sampler.

    Args:
        return_0based_negatives: If ``True``, returned negative samples have 0-based node
            and hyperedge IDs. If ``False``, they retain the original global IDs.
    """
    super().__init__()
    self.return_0based_negatives: bool = return_0based_negatives

sample(hdata, seed=None) abstractmethod

Abstract method for negative sampling.

Parameters:

Name Type Description Default
hdata HData

The input data object containing graph or hypergraph information.

required
seed int | None

Optional random seed for reproducible negative sampling. Defaults to None.

None

Returns:

Name Type Description
hdata HData

The negative samples as a new HData object.

Raises:

Type Description
NotImplementedError

If the method is not implemented in a subclass.

Source code in hypertorch/data/negative_sampler.py
@abstractmethod
def sample(self, hdata: HData, seed: int | None = None) -> HData:
    """
    Abstract method for negative sampling.

    Args:
        hdata: The input data object containing graph or hypergraph information.
        seed: Optional random seed for reproducible negative sampling. Defaults to ``None``.

    Returns:
        hdata: The negative samples as a new `HData` object.

    Raises:
        NotImplementedError: If the method is not implemented in a subclass.
    """
    raise NotImplementedError("Subclasses must implement this method.")

RandomNegativeSampler

Bases: SameNodeSpaceNegativeSampler

A random negative sampler. Negatives generated with return_0based_negatives = False aren't usable standalone as they have global node and hyperedge IDs. They must be concatenated with the original HData object that is provided as input to the sample method, as it contains the global node and hyperedge IDs and features that can be indexed with the negative samples' IDs.

Attributes:

Name Type Description
num_negative_samples int

Number of negative hyperedges to generate.

num_nodes_per_sample int

Number of nodes per negative hyperedge.

max_retry int

Maximum number of rejected sampling attempts allowed per requested negative hyperedge before failing.

Source code in hypertorch/data/negative_sampler.py
class RandomNegativeSampler(SameNodeSpaceNegativeSampler):
    """
    A random negative sampler. Negatives generated with ``return_0based_negatives = False``
    aren't usable standalone as they have global node and hyperedge IDs. They must be concatenated
    with the original `HData` object that is provided as input to the ``sample`` method, as it
    contains the global node and hyperedge IDs and features that can be indexed with
    the negative samples' IDs.

    Attributes:
        num_negative_samples: Number of negative hyperedges to generate.
        num_nodes_per_sample: Number of nodes per negative hyperedge.
        max_retry: Maximum number of rejected sampling attempts allowed per requested
            negative hyperedge before failing.
    """

    def __init__(
        self,
        num_negative_samples: int,
        num_nodes_per_sample: int,
        hyperedge_attr_enricher: HyperedgeAttrsEnricher | None = None,
        hyperedge_weights_enricher: HyperedgeWeightsEnricher | None = None,
        return_0based_negatives: bool = False,
        max_retry: int = 100,
    ):
        """
        Initialize the random negative sampler.

        Args:
            num_negative_samples: Number of negative hyperedges to generate.
            num_nodes_per_sample: Number of nodes per negative hyperedge.
            hyperedge_attr_enricher: An optional `HyperedgeAttrsEnricher` to generate attributes
                for the new hyperedges. If not provided, random attributes will be generated for
                the negative hyperedges if the input data has hyperedge attributes.
            hyperedge_weights_enricher: An optional `HyperedgeWeightsEnricher` to generate weights
                for the new hyperedges. If not provided, negative hyperedges will not have weights.
            return_0based_negatives: If ``True``, negative samples returned by ``sample`` will
                have 0-based node and hyperedge IDs. If ``False``, negative samples retain the
                original global node and hyperedge IDs from the input data.
            max_retry: Maximum number of rejected sampling attempts allowed per requested
                negative hyperedge before failing. If ``num_negative_samples`` is ``N``, the total
                maximum number of attempts will be ``N * max_retry``.

        Raises:
            ValueError: If any numeric argument is not positive.
        """
        if num_negative_samples <= 0:
            raise ValueError(f"num_negative_samples must be positive, got {num_negative_samples}.")
        if num_nodes_per_sample <= 0:
            raise ValueError(f"num_nodes_per_sample must be positive, got {num_nodes_per_sample}.")
        if max_retry <= 0:
            raise ValueError(f"max_retry must be positive, got {max_retry}.")

        super().__init__(
            hyperedge_attr_enricher=hyperedge_attr_enricher,
            hyperedge_weights_enricher=hyperedge_weights_enricher,
            return_0based_negatives=return_0based_negatives,
        )
        self.num_negative_samples: int = num_negative_samples
        self.num_nodes_per_sample: int = num_nodes_per_sample
        self.max_retry: int = max_retry

    def sample(self, hdata: HData, seed: int | None = None) -> HData:
        """
        Generate negative hyperedges by randomly sampling unique node IDs.
        Node IDs are sampled from the same node space as the input data, and the new negative
        hyperedge IDs start from the original number of hyperedges in the input data to
        avoid ID conflicts. The resulting negative samples are returned as a new `HData` object
        with remapped 0-based node and hyperedge IDs, if ``self.return_0based_negatives == True``.
        Otherwise, the negative samples retain their original global node and hyperedge IDs
        from the input data.

        Examples:
            With ``self.return_0based_negatives = True``:

            >>> num_negative_samples = 2
            >>> num_nodes_per_sample = 3
            >>> negative_hyperedge_index = [[0, 0, 1, 2, 3, 4],
            ...                             [0, 1, 1, 0, 1, 0]]

            The negative hyperedge 0 connects nodes 0, 2, 3.
            The second negative hyperedge 1 connects nodes 0, 1, 4.

            >>> negative_x = data.x[[0, 1, 2, 3, 4]]
            >>> negative_hyperedge_attr = random_attributes_for_2_negative_hyperedges

            With ``self.return_0based_negatives = False``:

            >>> num_negative_samples = 2
            >>> num_nodes_per_sample = 3
            >>> negative_hyperedge_index = [[100, 120, 300, 450, 500, 501],
            ...                             [3, 3, 3, 4, 4, 4]]

            Since node IDs are not remapped, the original feature matrix can be used directly.

            >>> negative_x = data.x

        Args:
            hdata: The input data object containing node and hyperedge information.
            seed: Optional random seed for reproducible negative sampling. Defaults to ``None``.

        Returns:
            hdata: A new `HData` instance containing the negative samples.

        Raises:
            ValueError: If ``num_nodes_per_sample`` is greater than the number of available nodes.
        """
        if self.num_nodes_per_sample > hdata.num_nodes:
            raise ValueError(
                f"Asked to create samples with {self.num_nodes_per_sample} nodes,"
                f" but only {hdata.num_nodes} nodes are available."
            )

        device = hdata.device

        # Existing positive hyperedges of the requested size must not be returned as negatives
        # Example: hyperedge_index = [[0, 1, 2, 0, 3],
        #                             [0, 0, 0, 1, 1]],
        #          num_nodes_per_sample = 3
        #          -> positive_hyperedge_signatures = {(0, 1, 2), (0, 3)}
        #          so (0, 1, 2) and (0, 3) are rejected even though they are cliques
        positive_hyperedge_signatures = self._hyperedges_signatures(hdata.hyperedge_index)
        matching_size_positive_hyperedges_signatures = {
            signature
            for signature in positive_hyperedge_signatures
            if len(signature) == self.num_nodes_per_sample
        }
        self.__validate_enough_negative_hyperedges(
            hdata=hdata,
            positive_hyperedges_signatures=matching_size_positive_hyperedges_signatures,
        )

        (
            sampled_hyperedge_indexes,
            sampled_hyperedge_attrs,
            sampled_negative_node_ids,
            new_hyperedge_id_offset,
        ) = self.__sample_loop(
            hdata=hdata,
            positive_hyperedges_signatures=matching_size_positive_hyperedges_signatures,
            seed=seed,
        )

        negative_node_ids_tensor = torch.tensor(
            sorted(sampled_negative_node_ids),
            dtype=torch.long,
            device=device,
        )
        new_x, num_negative_nodes = self._new_x(hdata.x, negative_node_ids_tensor)

        negative_hyperedge_ids = self._new_negative_hyperedge_ids(
            new_hyperedge_id_offset=new_hyperedge_id_offset,
            num_negative_samples=self.num_negative_samples,
            device=device,
        )

        negative_hyperedge_index = self._new_negative_hyperedge_index(
            sampled_hyperedge_indexes,
            negative_node_ids_tensor,
            negative_hyperedge_ids,
        )

        negative_hyperedge_attr = self._new_enriched_hyperedge_attr(
            hyperedge_attr_enricher=self.hyperedge_attr_enricher,
            negative_hyperedge_index=negative_hyperedge_index,
        )
        # Default to the random attributes if no enricher is provided and the input
        # data has hyperedge attributes
        if negative_hyperedge_attr is None:
            negative_hyperedge_attr = self._new_hyperedge_attr(
                sampled_hyperedge_attrs=sampled_hyperedge_attrs, hyperedge_attr=hdata.hyperedge_attr
            )

        return HData(
            x=new_x,
            hyperedge_index=negative_hyperedge_index,
            hyperedge_weights=self._new_enriched_hyperedge_weights(
                hyperedge_weights_enricher=self.hyperedge_weights_enricher,
                negative_hyperedge_index=negative_hyperedge_index,
            ),
            hyperedge_attr=negative_hyperedge_attr,
            num_nodes=num_negative_nodes,
            num_hyperedges=self.num_negative_samples,
            global_node_ids=self._new_global_node_ids(
                global_node_ids=hdata.global_node_ids, negative_node_ids=negative_node_ids_tensor
            ),
        ).with_y_zeros()

    def __sample_loop(
        self,
        hdata: HData,
        positive_hyperedges_signatures: set[tuple[int, ...]],
        seed: int | None = None,
    ) -> tuple[list[Tensor], list[Tensor], set[int], int]:
        """
        Sample unique negative hyperedges until the requested count or retry limit is reached.

        Args:
            hdata: The input hypergraph data used as the node and hyperedge ID source.
            positive_hyperedges_signatures: Existing positive hyperedge signatures that
                must not be sampled as negatives.
            seed: Optional random seed for reproducible sampling.

        Returns:
            sampled_hyperedge_indexes:  sampled hyperedge index tensors
            sampled_hyperedge_attrs: sampled hyperedge attribute tensors.
            sampled_negative_node_ids: sampled negative node IDs.
            new_hyperedge_id_offset: first negative hyperedge ID.

        Raises:
            ValueError: If the sampler cannot produce the requested number of unique negative
                hyperedges within the number of maximum allowed attempts.
        """
        device = hdata.device
        generator = create_seeded_torch_generator(device=device, seed=seed)

        sampled_negative_node_ids: set[int] = set()
        sampled_negative_hyperedge_signatures: set[tuple[int, ...]] = set()
        sampled_hyperedge_indexes: list[Tensor] = []
        sampled_hyperedge_attrs: list[Tensor] = []

        new_hyperedge_id_offset = hdata.num_hyperedges
        # max_retry is per requested negative, so scale the retries with the requested count.
        max_attempts = self.num_negative_samples * self.max_retry
        attempts = 0
        while (
            len(sampled_hyperedge_indexes) < self.num_negative_samples and attempts < max_attempts
        ):
            attempts += 1

            # Sample with multinomial without replacement to ensure unique node ids
            # and assign each node id equal probability of being
            # selected by setting all of them to 1
            # Example: num_nodes_per_sample=3, max_node_id=5
            #          -> possible output: [2, 0, 4]
            equal_probabilities = torch.ones(
                size=(hdata.num_nodes,),
                dtype=torch.float,
                device=device,
            )
            sampled_node_ids = torch.multinomial(
                input=equal_probabilities,
                num_samples=self.num_nodes_per_sample,
                replacement=False,
                generator=generator,
            )

            sampled_nodes_signature = self._hyperedge_nodes_signature(sampled_node_ids)
            if (
                sampled_nodes_signature in positive_hyperedges_signatures
                or sampled_nodes_signature in sampled_negative_hyperedge_signatures
            ):
                # Reject this sample as it already exists as a positive
                # or as a previously sampled negative hyperedge
                continue
            sampled_negative_hyperedge_signatures.add(sampled_nodes_signature)

            # Example: sampled_node_ids = [2, 0, 4], new_hyperedge_id=0, new_hyperedge_id_offset=3
            #          -> hyperedge_index = [[2, 0, 4],
            #                                [3, 3, 3]]  # this is sampled_hyperedge_id_tensor
            new_hyperedge_id = len(sampled_hyperedge_indexes)
            sampled_hyperedge_id_tensor = torch.full(
                size=(self.num_nodes_per_sample,),
                fill_value=new_hyperedge_id + new_hyperedge_id_offset,
                dtype=torch.long,
                device=device,
            )
            sampled_hyperedge_index = torch.stack(
                [sampled_node_ids, sampled_hyperedge_id_tensor], dim=0
            )
            sampled_hyperedge_indexes.append(sampled_hyperedge_index)

            # Example: nodes = [0, 1, 2],
            #          sampled_node_ids_0 = [0, 1], sampled_node_ids_1 = [1, 2],
            #          -> sampled_negative_node_ids = {0, 1, 2}
            sampled_negative_node_ids.update(sampled_node_ids.tolist())

            if hdata.hyperedge_attr is not None:
                random_hyperedge_attr = torch.randn(
                    size=hdata.hyperedge_attr[0].shape,
                    dtype=hdata.hyperedge_attr.dtype,
                    generator=generator,
                    device=device,
                )
                sampled_hyperedge_attrs.append(random_hyperedge_attr)

        if len(sampled_hyperedge_indexes) < self.num_negative_samples:
            raise ValueError(
                "Unable to sample "
                f"{self.num_negative_samples} unique negative hyperedges after "
                f"{max_attempts} attempts. Increase max_retry or request fewer samples."
            )

        return (
            sampled_hyperedge_indexes,
            sampled_hyperedge_attrs,
            sampled_negative_node_ids,
            new_hyperedge_id_offset,
        )

    def __validate_enough_negative_hyperedges(
        self,
        hdata: HData,
        positive_hyperedges_signatures: set[tuple[int, ...]],
    ) -> None:
        """
        Validate that enough unique negative hyperedges exist for the requested sample count.

        Args:
            hdata: The input hypergraph data that defines the number of available nodes.
            positive_hyperedges_signatures: Positive hyperedge signatures with the same size
                as the requested negative hyperedges.

        Raises:
            ValueError: If the requested number of negatives exceeds the number of possible
                unique non-positive hyperedges.
        """
        num_possible_hyperedges_by_size = comb(hdata.num_nodes, self.num_nodes_per_sample)
        num_positive_hyperedges = len(positive_hyperedges_signatures)
        num_possible_negative_hyperedges = num_possible_hyperedges_by_size - num_positive_hyperedges

        if self.num_negative_samples > num_possible_negative_hyperedges:
            raise ValueError(
                "Asked to create "
                f"{self.num_negative_samples} unique negative samples with "
                f"{self.num_nodes_per_sample} nodes each, but only "
                f"{num_possible_negative_hyperedges} are available."
            )

__init__(num_negative_samples, num_nodes_per_sample, hyperedge_attr_enricher=None, hyperedge_weights_enricher=None, return_0based_negatives=False, max_retry=100)

Initialize the random negative sampler.

Parameters:

Name Type Description Default
num_negative_samples int

Number of negative hyperedges to generate.

required
num_nodes_per_sample int

Number of nodes per negative hyperedge.

required
hyperedge_attr_enricher HyperedgeAttrsEnricher | None

An optional HyperedgeAttrsEnricher to generate attributes for the new hyperedges. If not provided, random attributes will be generated for the negative hyperedges if the input data has hyperedge attributes.

None
hyperedge_weights_enricher HyperedgeWeightsEnricher | None

An optional HyperedgeWeightsEnricher to generate weights for the new hyperedges. If not provided, negative hyperedges will not have weights.

None
return_0based_negatives bool

If True, negative samples returned by sample will have 0-based node and hyperedge IDs. If False, negative samples retain the original global node and hyperedge IDs from the input data.

False
max_retry int

Maximum number of rejected sampling attempts allowed per requested negative hyperedge before failing. If num_negative_samples is N, the total maximum number of attempts will be N * max_retry.

100

Raises:

Type Description
ValueError

If any numeric argument is not positive.

Source code in hypertorch/data/negative_sampler.py
def __init__(
    self,
    num_negative_samples: int,
    num_nodes_per_sample: int,
    hyperedge_attr_enricher: HyperedgeAttrsEnricher | None = None,
    hyperedge_weights_enricher: HyperedgeWeightsEnricher | None = None,
    return_0based_negatives: bool = False,
    max_retry: int = 100,
):
    """
    Initialize the random negative sampler.

    Args:
        num_negative_samples: Number of negative hyperedges to generate.
        num_nodes_per_sample: Number of nodes per negative hyperedge.
        hyperedge_attr_enricher: An optional `HyperedgeAttrsEnricher` to generate attributes
            for the new hyperedges. If not provided, random attributes will be generated for
            the negative hyperedges if the input data has hyperedge attributes.
        hyperedge_weights_enricher: An optional `HyperedgeWeightsEnricher` to generate weights
            for the new hyperedges. If not provided, negative hyperedges will not have weights.
        return_0based_negatives: If ``True``, negative samples returned by ``sample`` will
            have 0-based node and hyperedge IDs. If ``False``, negative samples retain the
            original global node and hyperedge IDs from the input data.
        max_retry: Maximum number of rejected sampling attempts allowed per requested
            negative hyperedge before failing. If ``num_negative_samples`` is ``N``, the total
            maximum number of attempts will be ``N * max_retry``.

    Raises:
        ValueError: If any numeric argument is not positive.
    """
    if num_negative_samples <= 0:
        raise ValueError(f"num_negative_samples must be positive, got {num_negative_samples}.")
    if num_nodes_per_sample <= 0:
        raise ValueError(f"num_nodes_per_sample must be positive, got {num_nodes_per_sample}.")
    if max_retry <= 0:
        raise ValueError(f"max_retry must be positive, got {max_retry}.")

    super().__init__(
        hyperedge_attr_enricher=hyperedge_attr_enricher,
        hyperedge_weights_enricher=hyperedge_weights_enricher,
        return_0based_negatives=return_0based_negatives,
    )
    self.num_negative_samples: int = num_negative_samples
    self.num_nodes_per_sample: int = num_nodes_per_sample
    self.max_retry: int = max_retry

sample(hdata, seed=None)

Generate negative hyperedges by randomly sampling unique node IDs. Node IDs are sampled from the same node space as the input data, and the new negative hyperedge IDs start from the original number of hyperedges in the input data to avoid ID conflicts. The resulting negative samples are returned as a new HData object with remapped 0-based node and hyperedge IDs, if self.return_0based_negatives == True. Otherwise, the negative samples retain their original global node and hyperedge IDs from the input data.

Examples:

With self.return_0based_negatives = True:

>>> num_negative_samples = 2
>>> num_nodes_per_sample = 3
>>> negative_hyperedge_index = [[0, 0, 1, 2, 3, 4],
...                             [0, 1, 1, 0, 1, 0]]

The negative hyperedge 0 connects nodes 0, 2, 3. The second negative hyperedge 1 connects nodes 0, 1, 4.

>>> negative_x = data.x[[0, 1, 2, 3, 4]]
>>> negative_hyperedge_attr = random_attributes_for_2_negative_hyperedges

With self.return_0based_negatives = False:

>>> num_negative_samples = 2
>>> num_nodes_per_sample = 3
>>> negative_hyperedge_index = [[100, 120, 300, 450, 500, 501],
...                             [3, 3, 3, 4, 4, 4]]

Since node IDs are not remapped, the original feature matrix can be used directly.

>>> negative_x = data.x

Parameters:

Name Type Description Default
hdata HData

The input data object containing node and hyperedge information.

required
seed int | None

Optional random seed for reproducible negative sampling. Defaults to None.

None

Returns:

Name Type Description
hdata HData

A new HData instance containing the negative samples.

Raises:

Type Description
ValueError

If num_nodes_per_sample is greater than the number of available nodes.

Source code in hypertorch/data/negative_sampler.py
def sample(self, hdata: HData, seed: int | None = None) -> HData:
    """
    Generate negative hyperedges by randomly sampling unique node IDs.
    Node IDs are sampled from the same node space as the input data, and the new negative
    hyperedge IDs start from the original number of hyperedges in the input data to
    avoid ID conflicts. The resulting negative samples are returned as a new `HData` object
    with remapped 0-based node and hyperedge IDs, if ``self.return_0based_negatives == True``.
    Otherwise, the negative samples retain their original global node and hyperedge IDs
    from the input data.

    Examples:
        With ``self.return_0based_negatives = True``:

        >>> num_negative_samples = 2
        >>> num_nodes_per_sample = 3
        >>> negative_hyperedge_index = [[0, 0, 1, 2, 3, 4],
        ...                             [0, 1, 1, 0, 1, 0]]

        The negative hyperedge 0 connects nodes 0, 2, 3.
        The second negative hyperedge 1 connects nodes 0, 1, 4.

        >>> negative_x = data.x[[0, 1, 2, 3, 4]]
        >>> negative_hyperedge_attr = random_attributes_for_2_negative_hyperedges

        With ``self.return_0based_negatives = False``:

        >>> num_negative_samples = 2
        >>> num_nodes_per_sample = 3
        >>> negative_hyperedge_index = [[100, 120, 300, 450, 500, 501],
        ...                             [3, 3, 3, 4, 4, 4]]

        Since node IDs are not remapped, the original feature matrix can be used directly.

        >>> negative_x = data.x

    Args:
        hdata: The input data object containing node and hyperedge information.
        seed: Optional random seed for reproducible negative sampling. Defaults to ``None``.

    Returns:
        hdata: A new `HData` instance containing the negative samples.

    Raises:
        ValueError: If ``num_nodes_per_sample`` is greater than the number of available nodes.
    """
    if self.num_nodes_per_sample > hdata.num_nodes:
        raise ValueError(
            f"Asked to create samples with {self.num_nodes_per_sample} nodes,"
            f" but only {hdata.num_nodes} nodes are available."
        )

    device = hdata.device

    # Existing positive hyperedges of the requested size must not be returned as negatives
    # Example: hyperedge_index = [[0, 1, 2, 0, 3],
    #                             [0, 0, 0, 1, 1]],
    #          num_nodes_per_sample = 3
    #          -> positive_hyperedge_signatures = {(0, 1, 2), (0, 3)}
    #          so (0, 1, 2) and (0, 3) are rejected even though they are cliques
    positive_hyperedge_signatures = self._hyperedges_signatures(hdata.hyperedge_index)
    matching_size_positive_hyperedges_signatures = {
        signature
        for signature in positive_hyperedge_signatures
        if len(signature) == self.num_nodes_per_sample
    }
    self.__validate_enough_negative_hyperedges(
        hdata=hdata,
        positive_hyperedges_signatures=matching_size_positive_hyperedges_signatures,
    )

    (
        sampled_hyperedge_indexes,
        sampled_hyperedge_attrs,
        sampled_negative_node_ids,
        new_hyperedge_id_offset,
    ) = self.__sample_loop(
        hdata=hdata,
        positive_hyperedges_signatures=matching_size_positive_hyperedges_signatures,
        seed=seed,
    )

    negative_node_ids_tensor = torch.tensor(
        sorted(sampled_negative_node_ids),
        dtype=torch.long,
        device=device,
    )
    new_x, num_negative_nodes = self._new_x(hdata.x, negative_node_ids_tensor)

    negative_hyperedge_ids = self._new_negative_hyperedge_ids(
        new_hyperedge_id_offset=new_hyperedge_id_offset,
        num_negative_samples=self.num_negative_samples,
        device=device,
    )

    negative_hyperedge_index = self._new_negative_hyperedge_index(
        sampled_hyperedge_indexes,
        negative_node_ids_tensor,
        negative_hyperedge_ids,
    )

    negative_hyperedge_attr = self._new_enriched_hyperedge_attr(
        hyperedge_attr_enricher=self.hyperedge_attr_enricher,
        negative_hyperedge_index=negative_hyperedge_index,
    )
    # Default to the random attributes if no enricher is provided and the input
    # data has hyperedge attributes
    if negative_hyperedge_attr is None:
        negative_hyperedge_attr = self._new_hyperedge_attr(
            sampled_hyperedge_attrs=sampled_hyperedge_attrs, hyperedge_attr=hdata.hyperedge_attr
        )

    return HData(
        x=new_x,
        hyperedge_index=negative_hyperedge_index,
        hyperedge_weights=self._new_enriched_hyperedge_weights(
            hyperedge_weights_enricher=self.hyperedge_weights_enricher,
            negative_hyperedge_index=negative_hyperedge_index,
        ),
        hyperedge_attr=negative_hyperedge_attr,
        num_nodes=num_negative_nodes,
        num_hyperedges=self.num_negative_samples,
        global_node_ids=self._new_global_node_ids(
            global_node_ids=hdata.global_node_ids, negative_node_ids=negative_node_ids_tensor
        ),
    ).with_y_zeros()

__sample_loop(hdata, positive_hyperedges_signatures, seed=None)

Sample unique negative hyperedges until the requested count or retry limit is reached.

Parameters:

Name Type Description Default
hdata HData

The input hypergraph data used as the node and hyperedge ID source.

required
positive_hyperedges_signatures set[tuple[int, ...]]

Existing positive hyperedge signatures that must not be sampled as negatives.

required
seed int | None

Optional random seed for reproducible sampling.

None

Returns:

Name Type Description
sampled_hyperedge_indexes list[Tensor]

sampled hyperedge index tensors

sampled_hyperedge_attrs list[Tensor]

sampled hyperedge attribute tensors.

sampled_negative_node_ids set[int]

sampled negative node IDs.

new_hyperedge_id_offset int

first negative hyperedge ID.

Raises:

Type Description
ValueError

If the sampler cannot produce the requested number of unique negative hyperedges within the number of maximum allowed attempts.

Source code in hypertorch/data/negative_sampler.py
def __sample_loop(
    self,
    hdata: HData,
    positive_hyperedges_signatures: set[tuple[int, ...]],
    seed: int | None = None,
) -> tuple[list[Tensor], list[Tensor], set[int], int]:
    """
    Sample unique negative hyperedges until the requested count or retry limit is reached.

    Args:
        hdata: The input hypergraph data used as the node and hyperedge ID source.
        positive_hyperedges_signatures: Existing positive hyperedge signatures that
            must not be sampled as negatives.
        seed: Optional random seed for reproducible sampling.

    Returns:
        sampled_hyperedge_indexes:  sampled hyperedge index tensors
        sampled_hyperedge_attrs: sampled hyperedge attribute tensors.
        sampled_negative_node_ids: sampled negative node IDs.
        new_hyperedge_id_offset: first negative hyperedge ID.

    Raises:
        ValueError: If the sampler cannot produce the requested number of unique negative
            hyperedges within the number of maximum allowed attempts.
    """
    device = hdata.device
    generator = create_seeded_torch_generator(device=device, seed=seed)

    sampled_negative_node_ids: set[int] = set()
    sampled_negative_hyperedge_signatures: set[tuple[int, ...]] = set()
    sampled_hyperedge_indexes: list[Tensor] = []
    sampled_hyperedge_attrs: list[Tensor] = []

    new_hyperedge_id_offset = hdata.num_hyperedges
    # max_retry is per requested negative, so scale the retries with the requested count.
    max_attempts = self.num_negative_samples * self.max_retry
    attempts = 0
    while (
        len(sampled_hyperedge_indexes) < self.num_negative_samples and attempts < max_attempts
    ):
        attempts += 1

        # Sample with multinomial without replacement to ensure unique node ids
        # and assign each node id equal probability of being
        # selected by setting all of them to 1
        # Example: num_nodes_per_sample=3, max_node_id=5
        #          -> possible output: [2, 0, 4]
        equal_probabilities = torch.ones(
            size=(hdata.num_nodes,),
            dtype=torch.float,
            device=device,
        )
        sampled_node_ids = torch.multinomial(
            input=equal_probabilities,
            num_samples=self.num_nodes_per_sample,
            replacement=False,
            generator=generator,
        )

        sampled_nodes_signature = self._hyperedge_nodes_signature(sampled_node_ids)
        if (
            sampled_nodes_signature in positive_hyperedges_signatures
            or sampled_nodes_signature in sampled_negative_hyperedge_signatures
        ):
            # Reject this sample as it already exists as a positive
            # or as a previously sampled negative hyperedge
            continue
        sampled_negative_hyperedge_signatures.add(sampled_nodes_signature)

        # Example: sampled_node_ids = [2, 0, 4], new_hyperedge_id=0, new_hyperedge_id_offset=3
        #          -> hyperedge_index = [[2, 0, 4],
        #                                [3, 3, 3]]  # this is sampled_hyperedge_id_tensor
        new_hyperedge_id = len(sampled_hyperedge_indexes)
        sampled_hyperedge_id_tensor = torch.full(
            size=(self.num_nodes_per_sample,),
            fill_value=new_hyperedge_id + new_hyperedge_id_offset,
            dtype=torch.long,
            device=device,
        )
        sampled_hyperedge_index = torch.stack(
            [sampled_node_ids, sampled_hyperedge_id_tensor], dim=0
        )
        sampled_hyperedge_indexes.append(sampled_hyperedge_index)

        # Example: nodes = [0, 1, 2],
        #          sampled_node_ids_0 = [0, 1], sampled_node_ids_1 = [1, 2],
        #          -> sampled_negative_node_ids = {0, 1, 2}
        sampled_negative_node_ids.update(sampled_node_ids.tolist())

        if hdata.hyperedge_attr is not None:
            random_hyperedge_attr = torch.randn(
                size=hdata.hyperedge_attr[0].shape,
                dtype=hdata.hyperedge_attr.dtype,
                generator=generator,
                device=device,
            )
            sampled_hyperedge_attrs.append(random_hyperedge_attr)

    if len(sampled_hyperedge_indexes) < self.num_negative_samples:
        raise ValueError(
            "Unable to sample "
            f"{self.num_negative_samples} unique negative hyperedges after "
            f"{max_attempts} attempts. Increase max_retry or request fewer samples."
        )

    return (
        sampled_hyperedge_indexes,
        sampled_hyperedge_attrs,
        sampled_negative_node_ids,
        new_hyperedge_id_offset,
    )

__validate_enough_negative_hyperedges(hdata, positive_hyperedges_signatures)

Validate that enough unique negative hyperedges exist for the requested sample count.

Parameters:

Name Type Description Default
hdata HData

The input hypergraph data that defines the number of available nodes.

required
positive_hyperedges_signatures set[tuple[int, ...]]

Positive hyperedge signatures with the same size as the requested negative hyperedges.

required

Raises:

Type Description
ValueError

If the requested number of negatives exceeds the number of possible unique non-positive hyperedges.

Source code in hypertorch/data/negative_sampler.py
def __validate_enough_negative_hyperedges(
    self,
    hdata: HData,
    positive_hyperedges_signatures: set[tuple[int, ...]],
) -> None:
    """
    Validate that enough unique negative hyperedges exist for the requested sample count.

    Args:
        hdata: The input hypergraph data that defines the number of available nodes.
        positive_hyperedges_signatures: Positive hyperedge signatures with the same size
            as the requested negative hyperedges.

    Raises:
        ValueError: If the requested number of negatives exceeds the number of possible
            unique non-positive hyperedges.
    """
    num_possible_hyperedges_by_size = comb(hdata.num_nodes, self.num_nodes_per_sample)
    num_positive_hyperedges = len(positive_hyperedges_signatures)
    num_possible_negative_hyperedges = num_possible_hyperedges_by_size - num_positive_hyperedges

    if self.num_negative_samples > num_possible_negative_hyperedges:
        raise ValueError(
            "Asked to create "
            f"{self.num_negative_samples} unique negative samples with "
            f"{self.num_nodes_per_sample} nodes each, but only "
            f"{num_possible_negative_hyperedges} are available."
        )

SameNodeSpaceNegativeSampler

Bases: NegativeSampler, ABC

Base class for negative samplers that sample only from existing nodes.

Attributes:

Name Type Description
hyperedge_attr_enricher HyperedgeAttrsEnricher | None

Optional enricher used to generate attributes for sampled hyperedges.

hyperedge_weights_enricher HyperedgeWeightsEnricher | None

Optional enricher used to generate weights for sampled hyperedges.

Source code in hypertorch/data/negative_sampler.py
class SameNodeSpaceNegativeSampler(NegativeSampler, ABC):
    """
    Base class for negative samplers that sample only from existing nodes.

    Attributes:
        hyperedge_attr_enricher: Optional enricher used to generate attributes for
            sampled hyperedges.
        hyperedge_weights_enricher: Optional enricher used to generate weights for
            sampled hyperedges.
    """

    def __init__(
        self,
        hyperedge_attr_enricher: HyperedgeAttrsEnricher | None = None,
        hyperedge_weights_enricher: HyperedgeWeightsEnricher | None = None,
        return_0based_negatives: bool = False,
    ):
        """
        Initialize the same-node-space negative sampler.

        Args:
            hyperedge_attr_enricher: Optional enricher used to generate attributes for
                sampled hyperedges.
            hyperedge_weights_enricher: Optional enricher used to generate weights for
                sampled hyperedges.
            return_0based_negatives: If ``True``, returned negatives are rebased to 0-based IDs.
                 If ``False``, they retain the original global IDs.
        """
        super().__init__(return_0based_negatives=return_0based_negatives)
        self.hyperedge_attr_enricher: HyperedgeAttrsEnricher | None = hyperedge_attr_enricher
        self.hyperedge_weights_enricher: HyperedgeWeightsEnricher | None = (
            hyperedge_weights_enricher
        )

__init__(hyperedge_attr_enricher=None, hyperedge_weights_enricher=None, return_0based_negatives=False)

Initialize the same-node-space negative sampler.

Parameters:

Name Type Description Default
hyperedge_attr_enricher HyperedgeAttrsEnricher | None

Optional enricher used to generate attributes for sampled hyperedges.

None
hyperedge_weights_enricher HyperedgeWeightsEnricher | None

Optional enricher used to generate weights for sampled hyperedges.

None
return_0based_negatives bool

If True, returned negatives are rebased to 0-based IDs. If False, they retain the original global IDs.

False
Source code in hypertorch/data/negative_sampler.py
def __init__(
    self,
    hyperedge_attr_enricher: HyperedgeAttrsEnricher | None = None,
    hyperedge_weights_enricher: HyperedgeWeightsEnricher | None = None,
    return_0based_negatives: bool = False,
):
    """
    Initialize the same-node-space negative sampler.

    Args:
        hyperedge_attr_enricher: Optional enricher used to generate attributes for
            sampled hyperedges.
        hyperedge_weights_enricher: Optional enricher used to generate weights for
            sampled hyperedges.
        return_0based_negatives: If ``True``, returned negatives are rebased to 0-based IDs.
             If ``False``, they retain the original global IDs.
    """
    super().__init__(return_0based_negatives=return_0based_negatives)
    self.hyperedge_attr_enricher: HyperedgeAttrsEnricher | None = hyperedge_attr_enricher
    self.hyperedge_weights_enricher: HyperedgeWeightsEnricher | None = (
        hyperedge_weights_enricher
    )

NegativeSamplingScheduler

Manages when to perform negative sampling during training based on a specified schedule.

This class allows for flexible scheduling of negative sampling, enabling it to be performed at different frequencies (e.g., every epoch, every N epochs, or only at the first epoch). The scheduler maintains a cache of the most recently sampled negatives, which can be reused across epochs if the schedule does not require resampling. This helps to optimize training by avoiding unnecessary sampling when the schedule dictates that negatives should only be generated at certain intervals.

Attributes:

Name Type Description
negative_sampler NegativeSampler

An instance of a NegativeSampler that defines how to sample negatives.

negative_sampling_schedule NegativeSamplingSchedule

Literal string specifying the schedule for sampling negatives.

negative_sampling_every_n int

An integer specifying the interval for sampling negatives when the schedule is set to "every_n_epochs". This parameter is ignored for other schedules.

Source code in hypertorch/data/negative_sampling_scheduler.py
class NegativeSamplingScheduler:
    """
    Manages when to perform negative sampling during training based on a specified schedule.

    This class allows for flexible scheduling of negative sampling, enabling it to be performed at
    different frequencies (e.g., every epoch, every N epochs, or only at the first epoch). The
    scheduler maintains a cache of the most recently sampled negatives, which can be reused across
    epochs if the schedule does not require resampling. This helps to optimize training by avoiding
    unnecessary sampling when the schedule dictates that negatives should only be generated at
    certain intervals.

    Attributes:
        negative_sampler: An instance of a ``NegativeSampler`` that defines how to sample negatives.
        negative_sampling_schedule: Literal string specifying the schedule for sampling negatives.
        negative_sampling_every_n: An integer specifying the interval for sampling negatives
            when the schedule is set to ``"every_n_epochs"``. This parameter is ignored
            for other schedules.
    """

    def __init__(
        self,
        negative_sampler: NegativeSampler,
        negative_sampling_schedule: NegativeSamplingSchedule = "every_epoch",
        negative_sampling_every_n: int = 1,
    ) -> None:
        """
        Initialize the scheduler.

        Args:
            negative_sampler: Sampler used to create negative samples.
            negative_sampling_schedule: Schedule controlling when negatives are sampled.
            negative_sampling_every_n: Epoch interval used for ``"every_n_epochs"`` scheduling.
        """
        self.negative_sampler: NegativeSampler = negative_sampler
        self.negative_sampling_schedule: NegativeSamplingSchedule = negative_sampling_schedule
        self.negative_sampling_every_n: int = negative_sampling_every_n

        self.__cached_negative_samples: HData | None = None

    @property
    def config(self) -> dict[str, Any]:
        """
        Returns the configuration of the negative sampling scheduler as a dictionary.
        """
        return {
            "negative_sampler": self.negative_sampler,
            "negative_sampling_schedule": self.negative_sampling_schedule,
            "negative_sampling_every_n": self.negative_sampling_every_n,
        }

    def should_sample(self, epoch: int) -> bool:
        """
        Whether to resample negatives for the current epoch.

        Args:
            epoch: The current epoch number, used to determine if sampling should occur based
                on the schedule.

        Returns:
            should_sample: True if negatives should be resampled for the current epoch,
                False otherwise.

        Raises:
            ValueError: If ``epoch`` is negative, ``negative_sampling_every_n`` is not
                positive for ``"every_n_epochs"``, or the schedule is unsupported.
        """
        if epoch < 0:
            raise ValueError(f"Epoch must be non-negative, got {epoch}.")

        match self.negative_sampling_schedule:
            case "every_n_epochs":
                if self.negative_sampling_every_n <= 0:
                    raise ValueError(
                        f"negative_sampling_every_n must be positive, "
                        f"got {self.negative_sampling_every_n}."
                    )
                return epoch % self.negative_sampling_every_n == 0
            case "first_epoch":
                return epoch == 0
            case "every_epoch":
                return True
            case _:
                raise ValueError(
                    f"Unsupported negative sampling schedule: {self.negative_sampling_schedule!r}."
                )

    def sample(self, batch: HData, epoch: int) -> HData:
        """
        Sample fresh negatives if the schedule requires it, otherwise return cache.

        Args:
            batch: The current batch of data for which to sample negatives.
            epoch: The current epoch number, used to determine if sampling should occur
                based on the schedule.

        Returns:
            negatives: A batch of negative samples, either freshly sampled or from cache.

        Raises:
            ValueError: If the schedule asks to reuse cached negatives before any negatives
                have been sampled.
        """
        if self.should_sample(epoch):
            self.__cached_negative_samples = self.negative_sampler.sample(batch)

        if self.__cached_negative_samples is None:
            raise ValueError(
                "Asked to sample negatives but no scheduling happen, "
                f"check that the configuration is correct: {self.config}"
            )

        return self.__cached_negative_samples

config property

Returns the configuration of the negative sampling scheduler as a dictionary.

__init__(negative_sampler, negative_sampling_schedule='every_epoch', negative_sampling_every_n=1)

Initialize the scheduler.

Parameters:

Name Type Description Default
negative_sampler NegativeSampler

Sampler used to create negative samples.

required
negative_sampling_schedule NegativeSamplingSchedule

Schedule controlling when negatives are sampled.

'every_epoch'
negative_sampling_every_n int

Epoch interval used for "every_n_epochs" scheduling.

1
Source code in hypertorch/data/negative_sampling_scheduler.py
def __init__(
    self,
    negative_sampler: NegativeSampler,
    negative_sampling_schedule: NegativeSamplingSchedule = "every_epoch",
    negative_sampling_every_n: int = 1,
) -> None:
    """
    Initialize the scheduler.

    Args:
        negative_sampler: Sampler used to create negative samples.
        negative_sampling_schedule: Schedule controlling when negatives are sampled.
        negative_sampling_every_n: Epoch interval used for ``"every_n_epochs"`` scheduling.
    """
    self.negative_sampler: NegativeSampler = negative_sampler
    self.negative_sampling_schedule: NegativeSamplingSchedule = negative_sampling_schedule
    self.negative_sampling_every_n: int = negative_sampling_every_n

    self.__cached_negative_samples: HData | None = None

should_sample(epoch)

Whether to resample negatives for the current epoch.

Parameters:

Name Type Description Default
epoch int

The current epoch number, used to determine if sampling should occur based on the schedule.

required

Returns:

Name Type Description
should_sample bool

True if negatives should be resampled for the current epoch, False otherwise.

Raises:

Type Description
ValueError

If epoch is negative, negative_sampling_every_n is not positive for "every_n_epochs", or the schedule is unsupported.

Source code in hypertorch/data/negative_sampling_scheduler.py
def should_sample(self, epoch: int) -> bool:
    """
    Whether to resample negatives for the current epoch.

    Args:
        epoch: The current epoch number, used to determine if sampling should occur based
            on the schedule.

    Returns:
        should_sample: True if negatives should be resampled for the current epoch,
            False otherwise.

    Raises:
        ValueError: If ``epoch`` is negative, ``negative_sampling_every_n`` is not
            positive for ``"every_n_epochs"``, or the schedule is unsupported.
    """
    if epoch < 0:
        raise ValueError(f"Epoch must be non-negative, got {epoch}.")

    match self.negative_sampling_schedule:
        case "every_n_epochs":
            if self.negative_sampling_every_n <= 0:
                raise ValueError(
                    f"negative_sampling_every_n must be positive, "
                    f"got {self.negative_sampling_every_n}."
                )
            return epoch % self.negative_sampling_every_n == 0
        case "first_epoch":
            return epoch == 0
        case "every_epoch":
            return True
        case _:
            raise ValueError(
                f"Unsupported negative sampling schedule: {self.negative_sampling_schedule!r}."
            )

sample(batch, epoch)

Sample fresh negatives if the schedule requires it, otherwise return cache.

Parameters:

Name Type Description Default
batch HData

The current batch of data for which to sample negatives.

required
epoch int

The current epoch number, used to determine if sampling should occur based on the schedule.

required

Returns:

Name Type Description
negatives HData

A batch of negative samples, either freshly sampled or from cache.

Raises:

Type Description
ValueError

If the schedule asks to reuse cached negatives before any negatives have been sampled.

Source code in hypertorch/data/negative_sampling_scheduler.py
def sample(self, batch: HData, epoch: int) -> HData:
    """
    Sample fresh negatives if the schedule requires it, otherwise return cache.

    Args:
        batch: The current batch of data for which to sample negatives.
        epoch: The current epoch number, used to determine if sampling should occur
            based on the schedule.

    Returns:
        negatives: A batch of negative samples, either freshly sampled or from cache.

    Raises:
        ValueError: If the schedule asks to reuse cached negatives before any negatives
            have been sampled.
    """
    if self.should_sample(epoch):
        self.__cached_negative_samples = self.negative_sampler.sample(batch)

    if self.__cached_negative_samples is None:
        raise ValueError(
            "Asked to sample negatives but no scheduling happen, "
            f"check that the configuration is correct: {self.config}"
        )

    return self.__cached_negative_samples

BaseSampler

Bases: ABC

Abstract base class for sampling from an HData instance.

Source code in hypertorch/data/sampler.py
class BaseSampler(ABC):
    """
    Abstract base class for sampling from an HData instance.
    """

    @abstractmethod
    def sample(self, index: int | list[int], hdata: HData) -> HData:
        """
        Sample a sub-hypergraph and return HData with global IDs.

        Args:
            index: An integer or list of integers specifying which items to sample.
            hdata: The original HData to sample from.

        Returns:
            hdata: A new HData instance containing only the sampled items and their associated data.

        Raises:
            NotImplementedError: If the method is not implemented by a subclass.
        """
        raise NotImplementedError("Subclasses must implement the sample method.")

    @abstractmethod
    def len(self, hdata: HData) -> int:
        """
        Return the number of sampleable items (nodes or hyperedges).

        Args:
            hdata: The HData to query for the number of sampleable items.

        Raises:
            NotImplementedError: If the method is not implemented by a subclass.
        """
        raise NotImplementedError("Subclasses must implement the len method.")

    def _normalize_index(self, index: int | list[int], size: int) -> list[int]:
        """
        Convert index to list, deduplicate, validate length.

        Args:
            index: An integer or a list of integers representing IDs to sample.
            size: The total number of sampleable items (e.g., nodes or hyperedges) for validation.

        Returns:
            ids: List of IDs to sample.

        Raises:
            ValueError: If the provided index is invalid (e.g., empty list or list length exceeds
                number of sampleable items).
            TypeError: If the index is not an integer or a list of integers.
        """
        if isinstance(index, list):
            if len(index) < 1:
                raise ValueError("Index list cannot be empty.")
            if len(index) > size:
                raise ValueError(
                    f"Index list length ({len(index)}) cannot exceed the number of "
                    f"sampleable items ({size})."
                )
            for id in index:
                if not isinstance(id, int) or isinstance(id, bool):
                    raise TypeError("Index list must contain only integers.")

            return list(set(index))

        if not isinstance(index, int) or isinstance(index, bool):
            raise TypeError("Index must be an integer or a list of integers.")

        return [index]

    def _sample_hyperedge_index(
        self,
        hyperedge_index: Tensor,
        sampled_hyperedge_ids: Tensor,
    ) -> Tensor:
        """
        Sample the hyperedge index to keep only incidences belonging to the specified sampled
        hyperedge IDs.

        Args:
            hyperedge_index: The original hyperedge index tensor of shape ``[2, num_incidences]``.
            sampled_hyperedge_ids: A tensor containing the IDs of hyperedges to sample.

        Returns:
            hyperedge_index: A new hyperedge index tensor containing only the incidences of the
                sampled hyperedges.
        """
        hyperedge_ids = hyperedge_index[1]

        # Find incidences where the hyperedge is in our sampled hyperedges
        # Example: hyperedge_ids = [0, 0, 0, 1, 2, 2], sampled_hyperedge_ids = [0, 2]
        #          -> sampled_hyperedges_mask = [True, True, True, False, True, True]
        sampled_hyperedges_mask = torch.isin(hyperedge_ids, sampled_hyperedge_ids)

        # Keep all incidences belonging to the sampled hyperedges
        # Example: hyperedge_index = [[0, 0, 1, 2, 3, 4],
        #                             [0, 0, 0, 1, 2, 2]],
        #          sampled_hyperedges_mask = [True, True, True, False, True, True]
        #          -> sampled_hyperedge_index = [[0, 0, 1, 3, 4],
        #                                        [0, 0, 0, 2, 2]]
        sampled_hyperedge_index = hyperedge_index[:, sampled_hyperedges_mask]
        return sampled_hyperedge_index

    def _validate_bounds(self, ids: list[int], size: int, label: str) -> None:
        """
        Check all IDs are in [0, self.len).

        Args:
            ids: List of IDs to validate.
            size: The total number of sampleable items (e.g., nodes or hyperedges).
            label: A string label for error messages (e.g., "Node ID" or "Hyperedge ID").

        Raises:
            IndexError: If any ID is out of bounds.
        """
        for id in ids:
            if id < 0 or id >= size:
                raise IndexError(f"{label} {id} is out of bounds (0, {size - 1}).")

sample(index, hdata) abstractmethod

Sample a sub-hypergraph and return HData with global IDs.

Parameters:

Name Type Description Default
index int | list[int]

An integer or list of integers specifying which items to sample.

required
hdata HData

The original HData to sample from.

required

Returns:

Name Type Description
hdata HData

A new HData instance containing only the sampled items and their associated data.

Raises:

Type Description
NotImplementedError

If the method is not implemented by a subclass.

Source code in hypertorch/data/sampler.py
@abstractmethod
def sample(self, index: int | list[int], hdata: HData) -> HData:
    """
    Sample a sub-hypergraph and return HData with global IDs.

    Args:
        index: An integer or list of integers specifying which items to sample.
        hdata: The original HData to sample from.

    Returns:
        hdata: A new HData instance containing only the sampled items and their associated data.

    Raises:
        NotImplementedError: If the method is not implemented by a subclass.
    """
    raise NotImplementedError("Subclasses must implement the sample method.")

len(hdata) abstractmethod

Return the number of sampleable items (nodes or hyperedges).

Parameters:

Name Type Description Default
hdata HData

The HData to query for the number of sampleable items.

required

Raises:

Type Description
NotImplementedError

If the method is not implemented by a subclass.

Source code in hypertorch/data/sampler.py
@abstractmethod
def len(self, hdata: HData) -> int:
    """
    Return the number of sampleable items (nodes or hyperedges).

    Args:
        hdata: The HData to query for the number of sampleable items.

    Raises:
        NotImplementedError: If the method is not implemented by a subclass.
    """
    raise NotImplementedError("Subclasses must implement the len method.")

HyperedgeSampler

Bases: BaseSampler

Sampler that selects hyperedges and their incident nodes.

Source code in hypertorch/data/sampler.py
class HyperedgeSampler(BaseSampler):
    """
    Sampler that selects hyperedges and their incident nodes.
    """

    def sample(self, index: int | list[int], hdata: HData) -> HData:
        """
        Sample hyperedges by their IDs and return the sub-hypergraph containing only those
        hyperedges and their incident nodes.

        Examples:
            >>> hyperedge_index = [[0, 0, 1, 2, 3, 4],
            ...                    [0, 0, 0, 1, 2, 2]]
            >>> hdata = HData.from_hyperedge_index(hyperedge_index)
            >>> strategy = HyperedgeSampler()
            >>> sampled_hdata = strategy.sample([0, 2], hdata)
            >>> sampled_hdata.hyperedge_index
            >>> tensor([[0, 0, 1, 3, 4],
            ...         [0, 0, 0, 2, 2]])

        Args:
            index: An integer or a list of integers representing hyperedge IDs to sample.
            hdata: The original HData to sample from.

        Returns:
            hdata: An HData instance containing only the sampled hyperedges and
                their incident nodes.

        Raises:
            ValueError: If the provided index is invalid (e.g., empty list or list length exceeds
                number of hyperedges).
            IndexError: If any hyperedge ID is out of bounds.
        """
        ids = self._normalize_index(index, self.len(hdata))
        self._validate_bounds(ids, self.len(hdata), "Hyperedge ID")

        hyperedge_index = hdata.hyperedge_index

        sampled_hyperedge_indexes = torch.tensor(
            ids,
            dtype=hyperedge_index.dtype,
            device=hyperedge_index.device,
        )
        sampled_hyperedge_ids = hdata.sampleable_hyperedge_ids[sampled_hyperedge_indexes]

        # Example: sampled_hyperedge_ids = [0, 2],
        #          hyperedge_index = [[0, 0, 1, 2, 3, 4],
        #                             [0, 0, 0, 1, 2, 2]],
        #          -> sampled_hyperedges_mask = [True, True, True, False, True, True]
        #          -> sampled_hyperedge_index = [[0, 0, 1, 3, 4],
        #                                        [0, 0, 0, 2, 2]]
        sampled_hyperedge_index = self._sample_hyperedge_index(
            hyperedge_index=hyperedge_index,
            sampled_hyperedge_ids=sampled_hyperedge_ids,
        )

        sampled_hdata = HData.from_hyperedge_index(
            hyperedge_index=sampled_hyperedge_index,
            task=hdata.task,
        )
        sampled_hdata_with_task_fields = self.__sample_task_specific_fields(
            sampled_hdata=sampled_hdata,
            sampled_hyperedge_index=sampled_hyperedge_index,
            sampled_hyperedge_ids=sampled_hyperedge_ids,
        )
        return sampled_hdata_with_task_fields

    def len(self, hdata: HData) -> int:
        """
        Return the number of sampleable hyperedges in the given HData.

        Args:
            hdata: The HData to query for the number of sampleable hyperedges.

        Returns:
            num_hyperedges: The number of sampleable hyperedges in the HData.
        """
        return hdata.num_sampleable_hyperedges

    def __sample_task_specific_fields(
        self,
        sampled_hdata: HData,
        sampled_hyperedge_index: Tensor,
        sampled_hyperedge_ids: Tensor,
    ) -> HData:
        """
        Sample task-specific fields for the given sampled HData.

        Args:
            sampled_hdata: The sampled HData to process.
            sampled_hyperedge_index: A tensor containing the hyperedge index
                of the sampled hyperedges.
            sampled_hyperedge_ids: A tensor containing the IDs of the sampled hyperedges.

        Returns:
            sampled_hdata: The HData with task-specific fields sampled.
        """
        if sampled_hdata.is_hyperedge_related_task:
            target_hyperedge_mask = self.__target_hyperedge_mask_for_sample(
                sampled_hdata=sampled_hdata,
                sampled_hyperedge_ids=sampled_hyperedge_ids,
                sampled_hyperedge_index=sampled_hyperedge_index,
            )
            return sampled_hdata.with_target_hyperedge_mask(target_hyperedge_mask)

        return sampled_hdata

    def __target_hyperedge_mask_for_sample(
        self,
        sampled_hdata: HData,
        sampled_hyperedge_ids: Tensor,
        sampled_hyperedge_index: Tensor,
    ) -> Tensor:
        sampled_hdata_hyperedge_incidences = sampled_hyperedge_index[1]
        sampled_hyperedge_ids_rebased_to_sampled_hdata_indexes = to_0based_ids(
            original_ids=sampled_hyperedge_ids,
            ids_to_rebase=sampled_hdata_hyperedge_incidences,
        )

        target_hyperedge_mask = torch.zeros(
            sampled_hdata.num_hyperedges,
            dtype=torch.bool,
            device=sampled_hdata.device,
        )
        target_hyperedge_mask[sampled_hyperedge_ids_rebased_to_sampled_hdata_indexes] = True

        return target_hyperedge_mask

sample(index, hdata)

Sample hyperedges by their IDs and return the sub-hypergraph containing only those hyperedges and their incident nodes.

Examples:

>>> hyperedge_index = [[0, 0, 1, 2, 3, 4],
...                    [0, 0, 0, 1, 2, 2]]
>>> hdata = HData.from_hyperedge_index(hyperedge_index)
>>> strategy = HyperedgeSampler()
>>> sampled_hdata = strategy.sample([0, 2], hdata)
>>> sampled_hdata.hyperedge_index
>>> tensor([[0, 0, 1, 3, 4],
...         [0, 0, 0, 2, 2]])

Parameters:

Name Type Description Default
index int | list[int]

An integer or a list of integers representing hyperedge IDs to sample.

required
hdata HData

The original HData to sample from.

required

Returns:

Name Type Description
hdata HData

An HData instance containing only the sampled hyperedges and their incident nodes.

Raises:

Type Description
ValueError

If the provided index is invalid (e.g., empty list or list length exceeds number of hyperedges).

IndexError

If any hyperedge ID is out of bounds.

Source code in hypertorch/data/sampler.py
def sample(self, index: int | list[int], hdata: HData) -> HData:
    """
    Sample hyperedges by their IDs and return the sub-hypergraph containing only those
    hyperedges and their incident nodes.

    Examples:
        >>> hyperedge_index = [[0, 0, 1, 2, 3, 4],
        ...                    [0, 0, 0, 1, 2, 2]]
        >>> hdata = HData.from_hyperedge_index(hyperedge_index)
        >>> strategy = HyperedgeSampler()
        >>> sampled_hdata = strategy.sample([0, 2], hdata)
        >>> sampled_hdata.hyperedge_index
        >>> tensor([[0, 0, 1, 3, 4],
        ...         [0, 0, 0, 2, 2]])

    Args:
        index: An integer or a list of integers representing hyperedge IDs to sample.
        hdata: The original HData to sample from.

    Returns:
        hdata: An HData instance containing only the sampled hyperedges and
            their incident nodes.

    Raises:
        ValueError: If the provided index is invalid (e.g., empty list or list length exceeds
            number of hyperedges).
        IndexError: If any hyperedge ID is out of bounds.
    """
    ids = self._normalize_index(index, self.len(hdata))
    self._validate_bounds(ids, self.len(hdata), "Hyperedge ID")

    hyperedge_index = hdata.hyperedge_index

    sampled_hyperedge_indexes = torch.tensor(
        ids,
        dtype=hyperedge_index.dtype,
        device=hyperedge_index.device,
    )
    sampled_hyperedge_ids = hdata.sampleable_hyperedge_ids[sampled_hyperedge_indexes]

    # Example: sampled_hyperedge_ids = [0, 2],
    #          hyperedge_index = [[0, 0, 1, 2, 3, 4],
    #                             [0, 0, 0, 1, 2, 2]],
    #          -> sampled_hyperedges_mask = [True, True, True, False, True, True]
    #          -> sampled_hyperedge_index = [[0, 0, 1, 3, 4],
    #                                        [0, 0, 0, 2, 2]]
    sampled_hyperedge_index = self._sample_hyperedge_index(
        hyperedge_index=hyperedge_index,
        sampled_hyperedge_ids=sampled_hyperedge_ids,
    )

    sampled_hdata = HData.from_hyperedge_index(
        hyperedge_index=sampled_hyperedge_index,
        task=hdata.task,
    )
    sampled_hdata_with_task_fields = self.__sample_task_specific_fields(
        sampled_hdata=sampled_hdata,
        sampled_hyperedge_index=sampled_hyperedge_index,
        sampled_hyperedge_ids=sampled_hyperedge_ids,
    )
    return sampled_hdata_with_task_fields

len(hdata)

Return the number of sampleable hyperedges in the given HData.

Parameters:

Name Type Description Default
hdata HData

The HData to query for the number of sampleable hyperedges.

required

Returns:

Name Type Description
num_hyperedges int

The number of sampleable hyperedges in the HData.

Source code in hypertorch/data/sampler.py
def len(self, hdata: HData) -> int:
    """
    Return the number of sampleable hyperedges in the given HData.

    Args:
        hdata: The HData to query for the number of sampleable hyperedges.

    Returns:
        num_hyperedges: The number of sampleable hyperedges in the HData.
    """
    return hdata.num_sampleable_hyperedges

__sample_task_specific_fields(sampled_hdata, sampled_hyperedge_index, sampled_hyperedge_ids)

Sample task-specific fields for the given sampled HData.

Parameters:

Name Type Description Default
sampled_hdata HData

The sampled HData to process.

required
sampled_hyperedge_index Tensor

A tensor containing the hyperedge index of the sampled hyperedges.

required
sampled_hyperedge_ids Tensor

A tensor containing the IDs of the sampled hyperedges.

required

Returns:

Name Type Description
sampled_hdata HData

The HData with task-specific fields sampled.

Source code in hypertorch/data/sampler.py
def __sample_task_specific_fields(
    self,
    sampled_hdata: HData,
    sampled_hyperedge_index: Tensor,
    sampled_hyperedge_ids: Tensor,
) -> HData:
    """
    Sample task-specific fields for the given sampled HData.

    Args:
        sampled_hdata: The sampled HData to process.
        sampled_hyperedge_index: A tensor containing the hyperedge index
            of the sampled hyperedges.
        sampled_hyperedge_ids: A tensor containing the IDs of the sampled hyperedges.

    Returns:
        sampled_hdata: The HData with task-specific fields sampled.
    """
    if sampled_hdata.is_hyperedge_related_task:
        target_hyperedge_mask = self.__target_hyperedge_mask_for_sample(
            sampled_hdata=sampled_hdata,
            sampled_hyperedge_ids=sampled_hyperedge_ids,
            sampled_hyperedge_index=sampled_hyperedge_index,
        )
        return sampled_hdata.with_target_hyperedge_mask(target_hyperedge_mask)

    return sampled_hdata

NodeSampler

Bases: BaseSampler

Sampler that selects nodes and their incident hyperedges.

Source code in hypertorch/data/sampler.py
class NodeSampler(BaseSampler):
    """
    Sampler that selects nodes and their incident hyperedges.
    """

    def sample(self, index: int | list[int], hdata: HData) -> HData:
        """
        Sample nodes by their IDs and return the sub-hypergraph containing only those nodes and
        their incident hyperedges.

        Examples:
            >>> hyperedge_index = [[0, 0, 1, 2, 3, 4],
            ...                    [0, 0, 0, 1, 2, 2]]
            >>> hdata = HData.from_hyperedge_index(hyperedge_index)
            >>> strategy = NodeSampler()
            >>> sampled_hdata = strategy.sample([0, 3], hdata)
            >>> sampled_hdata.hyperedge_index
            >>> tensor([[0, 0, 1, 3, 4],
            ...         [0, 0, 0, 2, 2]])

        Args:
            index: An integer or a list of integers representing node IDs to sample.
            hdata: The original HData to sample from.

        Returns:
            hdata: An HData instance containing only the sampled nodes and their
                incident hyperedges.

        Raises:
            ValueError: If the provided index is invalid (e.g., empty list or list length exceeds
                number of nodes).
            IndexError: If any node ID is out of bounds.
        """
        ids = self._normalize_index(index, self.len(hdata))
        self._validate_bounds(ids, self.len(hdata), "Node ID")

        sampled_hdata, sampled_hyperedge_index, sampled_node_ids = self.__sample_hdata(
            hdata=hdata,
            ids_to_sample=ids,
        )
        sampled_hdata = self.__sample_task_specific_fields(
            sampled_hdata=sampled_hdata,
            sampled_hyperedge_index=sampled_hyperedge_index,
            sampled_node_ids=sampled_node_ids,
        )
        return sampled_hdata

    def len(self, hdata: HData) -> int:
        """
        Return the number of nodes in the given HData.

        Args:
            hdata: The HData to query for the number of nodes.

        Returns:
            num_nodes: The number of nodes in the HData.
        """
        return hdata.num_sampleable_nodes

    def __sample_hdata(
        self,
        hdata: HData,
        ids_to_sample: list[int],
    ) -> tuple[HData, Tensor, Tensor]:
        """
        Sample nodes from the given HData and return a new HData instance
        containing only the sampled nodes and their incident hyperedges.

        Args:
            hdata: The HData to sample from.
            ids_to_sample: A list of node IDs to sample.

        Returns:
            sampled_hdata: An HData instance containing only the sampled nodes
                and their incident hyperedges.
            sampled_hyperedge_index: A tensor containing the hyperedge index of the
                sampled hyperedges.
            sampled_node_ids: A tensor containing the IDs of the sampled nodes.
        """
        sampled_node_indexes = torch.tensor(ids_to_sample, dtype=torch.long, device=hdata.device)
        sampled_node_ids = hdata.sampleable_node_ids[sampled_node_indexes]

        hyperedge_index = hdata.hyperedge_index
        node_ids = hyperedge_index[0]
        hyperedge_ids = hyperedge_index[1]

        # Find incidences where the node is in our sampled nodes
        # Example: node_ids = [0, 0, 1, 2, 3, 4], sampled_node_ids = [0, 3]
        #          -> sampled_nodes_mask = [True, True, False, False, True, False]
        sampled_nodes_mask = torch.isin(node_ids, sampled_node_ids)

        # Get unique hyperedges that have at least one sampled node
        # Example: hyperedge_ids = [0, 0, 0, 1, 2, 2],
        #  sampled_nodes_mask = [True, True, False, False, True, False]
        #          -> sampled_hyperedge_ids = [0, 2] as they connect to sampled nodes
        sampled_hyperedge_ids = hyperedge_ids[sampled_nodes_mask].unique()

        # Example: sampled_hyperedge_ids = [0, 2],
        #          hyperedge_index = [[0, 0, 1, 2, 3, 4],
        #                             [0, 0, 0, 1, 2, 2]],
        #          -> sampled_hyperedges_mask = [True, True, True, False, True, True]
        #          -> sampled_hyperedge_index = [[0, 0, 1, 3, 4],
        #                                        [0, 0, 0, 2, 2]]
        sampled_hyperedge_index = self._sample_hyperedge_index(
            hyperedge_index=hyperedge_index,
            sampled_hyperedge_ids=sampled_hyperedge_ids,
        )

        sampled_hdata = HData.from_hyperedge_index(
            hyperedge_index=sampled_hyperedge_index,
            task=hdata.task,
        )
        return sampled_hdata, sampled_hyperedge_index, sampled_node_ids

    def __sample_task_specific_fields(
        self,
        sampled_hdata: HData,
        sampled_hyperedge_index: Tensor,
        sampled_node_ids: Tensor,
    ) -> HData:
        """
        Sample task-specific fields for the given sampled HData.
        For examples, if the task is node-related, it will create a
        target node mask for the sampled nodes.

        Args:
            sampled_hdata: The sampled HData to process.
            sampled_hyperedge_index: A tensor containing the hyperedge index
                of the sampled hyperedges.
            sampled_node_ids: A tensor containing the IDs of the sampled nodes.

        Returns:
            sampled_hdata: The HData with task-specific fields sampled.
        """
        if sampled_hdata.is_node_related_task:
            target_node_mask = self.__target_node_mask_for_sample(
                sampled_hdata=sampled_hdata,
                sampled_node_ids=sampled_node_ids,
                sampled_hyperedge_index=sampled_hyperedge_index,
            )
            return sampled_hdata.with_target_node_mask(target_node_mask)

        return sampled_hdata

    def __target_node_mask_for_sample(
        self,
        sampled_hdata: HData,
        sampled_node_ids: Tensor,
        sampled_hyperedge_index: Tensor,
    ) -> Tensor:
        sampled_hdata_node_incidences = sampled_hyperedge_index[0]
        sampled_node_ids_rebased_to_sampled_hdata_indexes = to_0based_ids(
            original_ids=sampled_node_ids,
            ids_to_rebase=sampled_hdata_node_incidences,
        )

        # Mark only the originally sampled seed nodes as targets (target_node_mask=True)
        # in the sampled HData's local node space. Context nodes added because they share
        # incident hyperedges remain marked as non-target (target_node_mask=False).
        # Example: sampled_node_ids = [0, 3],
        #          sampled_hdata_node_incidences = sampled_hyperedge_index[0] = [0, 0, 1, 3, 4]
        #          so node_ids in sampled_hdata are [0, 1, 3, 4]
        #          sampled_node_ids_rebased_to_sampled_hdata_indexes = [0, 2]  # 0 -> 0
        #                                                                      # 3 -> 2
        #          target_node_mask = [True, False, True, False]
        target_node_mask = torch.zeros(
            sampled_hdata.num_nodes,
            dtype=torch.bool,
            device=sampled_hdata.device,
        )
        target_node_mask[sampled_node_ids_rebased_to_sampled_hdata_indexes] = True

        return target_node_mask

sample(index, hdata)

Sample nodes by their IDs and return the sub-hypergraph containing only those nodes and their incident hyperedges.

Examples:

>>> hyperedge_index = [[0, 0, 1, 2, 3, 4],
...                    [0, 0, 0, 1, 2, 2]]
>>> hdata = HData.from_hyperedge_index(hyperedge_index)
>>> strategy = NodeSampler()
>>> sampled_hdata = strategy.sample([0, 3], hdata)
>>> sampled_hdata.hyperedge_index
>>> tensor([[0, 0, 1, 3, 4],
...         [0, 0, 0, 2, 2]])

Parameters:

Name Type Description Default
index int | list[int]

An integer or a list of integers representing node IDs to sample.

required
hdata HData

The original HData to sample from.

required

Returns:

Name Type Description
hdata HData

An HData instance containing only the sampled nodes and their incident hyperedges.

Raises:

Type Description
ValueError

If the provided index is invalid (e.g., empty list or list length exceeds number of nodes).

IndexError

If any node ID is out of bounds.

Source code in hypertorch/data/sampler.py
def sample(self, index: int | list[int], hdata: HData) -> HData:
    """
    Sample nodes by their IDs and return the sub-hypergraph containing only those nodes and
    their incident hyperedges.

    Examples:
        >>> hyperedge_index = [[0, 0, 1, 2, 3, 4],
        ...                    [0, 0, 0, 1, 2, 2]]
        >>> hdata = HData.from_hyperedge_index(hyperedge_index)
        >>> strategy = NodeSampler()
        >>> sampled_hdata = strategy.sample([0, 3], hdata)
        >>> sampled_hdata.hyperedge_index
        >>> tensor([[0, 0, 1, 3, 4],
        ...         [0, 0, 0, 2, 2]])

    Args:
        index: An integer or a list of integers representing node IDs to sample.
        hdata: The original HData to sample from.

    Returns:
        hdata: An HData instance containing only the sampled nodes and their
            incident hyperedges.

    Raises:
        ValueError: If the provided index is invalid (e.g., empty list or list length exceeds
            number of nodes).
        IndexError: If any node ID is out of bounds.
    """
    ids = self._normalize_index(index, self.len(hdata))
    self._validate_bounds(ids, self.len(hdata), "Node ID")

    sampled_hdata, sampled_hyperedge_index, sampled_node_ids = self.__sample_hdata(
        hdata=hdata,
        ids_to_sample=ids,
    )
    sampled_hdata = self.__sample_task_specific_fields(
        sampled_hdata=sampled_hdata,
        sampled_hyperedge_index=sampled_hyperedge_index,
        sampled_node_ids=sampled_node_ids,
    )
    return sampled_hdata

len(hdata)

Return the number of nodes in the given HData.

Parameters:

Name Type Description Default
hdata HData

The HData to query for the number of nodes.

required

Returns:

Name Type Description
num_nodes int

The number of nodes in the HData.

Source code in hypertorch/data/sampler.py
def len(self, hdata: HData) -> int:
    """
    Return the number of nodes in the given HData.

    Args:
        hdata: The HData to query for the number of nodes.

    Returns:
        num_nodes: The number of nodes in the HData.
    """
    return hdata.num_sampleable_nodes

__sample_hdata(hdata, ids_to_sample)

Sample nodes from the given HData and return a new HData instance containing only the sampled nodes and their incident hyperedges.

Parameters:

Name Type Description Default
hdata HData

The HData to sample from.

required
ids_to_sample list[int]

A list of node IDs to sample.

required

Returns:

Name Type Description
sampled_hdata HData

An HData instance containing only the sampled nodes and their incident hyperedges.

sampled_hyperedge_index Tensor

A tensor containing the hyperedge index of the sampled hyperedges.

sampled_node_ids Tensor

A tensor containing the IDs of the sampled nodes.

Source code in hypertorch/data/sampler.py
def __sample_hdata(
    self,
    hdata: HData,
    ids_to_sample: list[int],
) -> tuple[HData, Tensor, Tensor]:
    """
    Sample nodes from the given HData and return a new HData instance
    containing only the sampled nodes and their incident hyperedges.

    Args:
        hdata: The HData to sample from.
        ids_to_sample: A list of node IDs to sample.

    Returns:
        sampled_hdata: An HData instance containing only the sampled nodes
            and their incident hyperedges.
        sampled_hyperedge_index: A tensor containing the hyperedge index of the
            sampled hyperedges.
        sampled_node_ids: A tensor containing the IDs of the sampled nodes.
    """
    sampled_node_indexes = torch.tensor(ids_to_sample, dtype=torch.long, device=hdata.device)
    sampled_node_ids = hdata.sampleable_node_ids[sampled_node_indexes]

    hyperedge_index = hdata.hyperedge_index
    node_ids = hyperedge_index[0]
    hyperedge_ids = hyperedge_index[1]

    # Find incidences where the node is in our sampled nodes
    # Example: node_ids = [0, 0, 1, 2, 3, 4], sampled_node_ids = [0, 3]
    #          -> sampled_nodes_mask = [True, True, False, False, True, False]
    sampled_nodes_mask = torch.isin(node_ids, sampled_node_ids)

    # Get unique hyperedges that have at least one sampled node
    # Example: hyperedge_ids = [0, 0, 0, 1, 2, 2],
    #  sampled_nodes_mask = [True, True, False, False, True, False]
    #          -> sampled_hyperedge_ids = [0, 2] as they connect to sampled nodes
    sampled_hyperedge_ids = hyperedge_ids[sampled_nodes_mask].unique()

    # Example: sampled_hyperedge_ids = [0, 2],
    #          hyperedge_index = [[0, 0, 1, 2, 3, 4],
    #                             [0, 0, 0, 1, 2, 2]],
    #          -> sampled_hyperedges_mask = [True, True, True, False, True, True]
    #          -> sampled_hyperedge_index = [[0, 0, 1, 3, 4],
    #                                        [0, 0, 0, 2, 2]]
    sampled_hyperedge_index = self._sample_hyperedge_index(
        hyperedge_index=hyperedge_index,
        sampled_hyperedge_ids=sampled_hyperedge_ids,
    )

    sampled_hdata = HData.from_hyperedge_index(
        hyperedge_index=sampled_hyperedge_index,
        task=hdata.task,
    )
    return sampled_hdata, sampled_hyperedge_index, sampled_node_ids

__sample_task_specific_fields(sampled_hdata, sampled_hyperedge_index, sampled_node_ids)

Sample task-specific fields for the given sampled HData. For examples, if the task is node-related, it will create a target node mask for the sampled nodes.

Parameters:

Name Type Description Default
sampled_hdata HData

The sampled HData to process.

required
sampled_hyperedge_index Tensor

A tensor containing the hyperedge index of the sampled hyperedges.

required
sampled_node_ids Tensor

A tensor containing the IDs of the sampled nodes.

required

Returns:

Name Type Description
sampled_hdata HData

The HData with task-specific fields sampled.

Source code in hypertorch/data/sampler.py
def __sample_task_specific_fields(
    self,
    sampled_hdata: HData,
    sampled_hyperedge_index: Tensor,
    sampled_node_ids: Tensor,
) -> HData:
    """
    Sample task-specific fields for the given sampled HData.
    For examples, if the task is node-related, it will create a
    target node mask for the sampled nodes.

    Args:
        sampled_hdata: The sampled HData to process.
        sampled_hyperedge_index: A tensor containing the hyperedge index
            of the sampled hyperedges.
        sampled_node_ids: A tensor containing the IDs of the sampled nodes.

    Returns:
        sampled_hdata: The HData with task-specific fields sampled.
    """
    if sampled_hdata.is_node_related_task:
        target_node_mask = self.__target_node_mask_for_sample(
            sampled_hdata=sampled_hdata,
            sampled_node_ids=sampled_node_ids,
            sampled_hyperedge_index=sampled_hyperedge_index,
        )
        return sampled_hdata.with_target_node_mask(target_node_mask)

    return sampled_hdata

SamplingStrategyEnum

Bases: StrEnum

Sampling strategies supported by datasets.

Source code in hypertorch/data/sampler.py
class SamplingStrategyEnum(StrEnum):
    """
    Sampling strategies supported by datasets.
    """

    NODE = "node"
    HYPEREDGE = "hyperedge"

HyperedgeDatasetSplitter

Bases: Splitter['Dataset', tuple[list['Dataset'], list[float]]]

Split a dataset into dense transductive hyperedge partitions.

In the transductive setting, every split keeps the full hypergraph as model context and identifies its supervised hyperedges with HData.target_hyperedge_mask. In the inductive setting, splits are materialized as local sub-hypergraphs.

Attributes:

Name Type Description
node_space_setting NodeSpaceSetting

Whether to preserve full or local node spaces.

shuffle bool | None

Whether to shuffle hyperedges before splitting.

seed int | None

Random seed used when shuffling.

Source code in hypertorch/data/splitter.py
class HyperedgeDatasetSplitter(Splitter["Dataset", tuple[list["Dataset"], list[float]]]):
    """
    Split a dataset into dense transductive hyperedge partitions.

    In the transductive setting, every split keeps the full hypergraph as model context and
    identifies its supervised hyperedges with ``HData.target_hyperedge_mask``. In the inductive
    setting, splits are materialized as local sub-hypergraphs.

    Attributes:
        node_space_setting: Whether to preserve full or local node spaces.
        shuffle: Whether to shuffle hyperedges before splitting.
        seed: Random seed used when shuffling.
    """

    def __init__(
        self,
        node_space_setting: NodeSpaceSetting = "transductive",
        shuffle: bool | None = False,
        seed: int | None = None,
    ) -> None:
        """
        Initialize the dataset splitter.

        Args:
            node_space_setting: Whether to preserve full or local node spaces.
            shuffle: Whether to shuffle hyperedges before splitting.
            seed: Optional random seed for reproducibility.
        """
        self.node_space_setting: NodeSpaceSetting = node_space_setting
        self.shuffle: bool | None = shuffle
        self.seed: int | None = seed

        validate_node_space_setting(self.node_space_setting)

    def split(self, to_split: Dataset, **kwargs: Any) -> tuple[list[Dataset], list[float]]:
        """
        Split a dataset and return materialized split datasets plus final ratios.

        Args:
            to_split: The `Dataset` to split.

            kwargs:
                ratios: Desired split ratios, used for initial split construction and
                    as a reference during rebalancing. Expected as a keyword argument.
                    List of floats summing to ``1.0``.
        Returns:
            split_datasets: The list of split datasets.
            final_ratios: The list of final target-hyperedge-count ratios.

        Raises:
            ValueError: If ratios do not sum to ``1.0``, a final split has zero target hyperedges.
                Or if the dataset task is not hyperedge-related.
        """
        if not to_split.hdata.is_hyperedge_related_task:
            raise ValueError(
                f"Cannot split dataset with task '{to_split.hdata.task}' because it is not "
                "a hyperedge-related task. Use 'NodeDatasetSplitter' for node-related tasks."
            )

        ratios: list[float] = kwargs.get("ratios", [])
        validate_ratios(ratios)

        hdata = to_split.hdata
        hyperedge_splitter = HyperedgeIDSplitter(
            hyperedge_index=hdata.hyperedge_index,
            num_nodes=hdata.num_nodes,
            num_hyperedges=hdata.num_hyperedges,
        )
        hyperedge_ids_permutation = hyperedge_splitter.get_sampleable_hyperedge_ids_permutation(
            sampleable_hyperedge_ids=hdata.sampleable_hyperedge_ids,
            shuffle=self.shuffle,
            seed=self.seed,
        )
        hyperedge_ids_by_split, final_ratios = hyperedge_splitter.split(
            to_split=hyperedge_ids_permutation,
            ratios=ratios,
        )
        hyperedge_splitter.validate_splits_have_hyperedges(hyperedge_ids_by_split)

        split_datasets: list[Dataset] = []
        for split_hyperedge_ids in hyperedge_ids_by_split:
            split_hdata = HyperedgeHDataSplitter(node_space_setting=self.node_space_setting).split(
                to_split=hdata,
                split_hyperedge_ids=split_hyperedge_ids,
            )
            split_hdata = split_hdata.to(device=hdata.device)

            split_dataset = to_split.__class__(
                hdata=split_hdata,
                sampling_strategy=to_split.sampling_strategy,
            )
            split_datasets.append(split_dataset)

        return split_datasets, final_ratios

__init__(node_space_setting='transductive', shuffle=False, seed=None)

Initialize the dataset splitter.

Parameters:

Name Type Description Default
node_space_setting NodeSpaceSetting

Whether to preserve full or local node spaces.

'transductive'
shuffle bool | None

Whether to shuffle hyperedges before splitting.

False
seed int | None

Optional random seed for reproducibility.

None
Source code in hypertorch/data/splitter.py
def __init__(
    self,
    node_space_setting: NodeSpaceSetting = "transductive",
    shuffle: bool | None = False,
    seed: int | None = None,
) -> None:
    """
    Initialize the dataset splitter.

    Args:
        node_space_setting: Whether to preserve full or local node spaces.
        shuffle: Whether to shuffle hyperedges before splitting.
        seed: Optional random seed for reproducibility.
    """
    self.node_space_setting: NodeSpaceSetting = node_space_setting
    self.shuffle: bool | None = shuffle
    self.seed: int | None = seed

    validate_node_space_setting(self.node_space_setting)

split(to_split, **kwargs)

Split a dataset and return materialized split datasets plus final ratios.

Parameters:

Name Type Description Default
to_split Dataset

The Dataset to split.

required
kwargs Any

ratios: Desired split ratios, used for initial split construction and as a reference during rebalancing. Expected as a keyword argument. List of floats summing to 1.0.

{}

Returns: split_datasets: The list of split datasets. final_ratios: The list of final target-hyperedge-count ratios.

Raises:

Type Description
ValueError

If ratios do not sum to 1.0, a final split has zero target hyperedges. Or if the dataset task is not hyperedge-related.

Source code in hypertorch/data/splitter.py
def split(self, to_split: Dataset, **kwargs: Any) -> tuple[list[Dataset], list[float]]:
    """
    Split a dataset and return materialized split datasets plus final ratios.

    Args:
        to_split: The `Dataset` to split.

        kwargs:
            ratios: Desired split ratios, used for initial split construction and
                as a reference during rebalancing. Expected as a keyword argument.
                List of floats summing to ``1.0``.
    Returns:
        split_datasets: The list of split datasets.
        final_ratios: The list of final target-hyperedge-count ratios.

    Raises:
        ValueError: If ratios do not sum to ``1.0``, a final split has zero target hyperedges.
            Or if the dataset task is not hyperedge-related.
    """
    if not to_split.hdata.is_hyperedge_related_task:
        raise ValueError(
            f"Cannot split dataset with task '{to_split.hdata.task}' because it is not "
            "a hyperedge-related task. Use 'NodeDatasetSplitter' for node-related tasks."
        )

    ratios: list[float] = kwargs.get("ratios", [])
    validate_ratios(ratios)

    hdata = to_split.hdata
    hyperedge_splitter = HyperedgeIDSplitter(
        hyperedge_index=hdata.hyperedge_index,
        num_nodes=hdata.num_nodes,
        num_hyperedges=hdata.num_hyperedges,
    )
    hyperedge_ids_permutation = hyperedge_splitter.get_sampleable_hyperedge_ids_permutation(
        sampleable_hyperedge_ids=hdata.sampleable_hyperedge_ids,
        shuffle=self.shuffle,
        seed=self.seed,
    )
    hyperedge_ids_by_split, final_ratios = hyperedge_splitter.split(
        to_split=hyperedge_ids_permutation,
        ratios=ratios,
    )
    hyperedge_splitter.validate_splits_have_hyperedges(hyperedge_ids_by_split)

    split_datasets: list[Dataset] = []
    for split_hyperedge_ids in hyperedge_ids_by_split:
        split_hdata = HyperedgeHDataSplitter(node_space_setting=self.node_space_setting).split(
            to_split=hdata,
            split_hyperedge_ids=split_hyperedge_ids,
        )
        split_hdata = split_hdata.to(device=hdata.device)

        split_dataset = to_split.__class__(
            hdata=split_hdata,
            sampling_strategy=to_split.sampling_strategy,
        )
        split_datasets.append(split_dataset)

    return split_datasets, final_ratios

HyperedgeHDataSplitter

Bases: Splitter['HData', 'HData']

Materialize an HData split from explicit hyperedge IDs.

Attributes:

Name Type Description
node_space_setting NodeSpaceSetting

Whether to preserve the full node space in the split.

Source code in hypertorch/data/splitter.py
class HyperedgeHDataSplitter(Splitter["HData", "HData"]):
    """
    Materialize an HData split from explicit hyperedge IDs.

    Attributes:
        node_space_setting: Whether to preserve the full node space in the split.
    """

    def __init__(self, node_space_setting: NodeSpaceSetting = "transductive") -> None:
        """
        Initialize the HData splitter.

        Args:
            node_space_setting: Whether to preserve the full node space in the split.
        """
        self.node_space_setting: NodeSpaceSetting = node_space_setting
        validate_node_space_setting(self.node_space_setting)

    def split(self, to_split: HData, **kwargs: Any) -> HData:
        """
        Build an HData for a single split from the given hyperedge IDs.

        Args:
            to_split: The original HData containing the full hypergraph.
            kwargs:
                split_hyperedge_ids: The hyperedge IDs that should be included in the split,
                    expected as a keyword argument.

        Returns:
            hdata: The splitted instance with remapped node and hyperedge IDs.
        """
        if not to_split.is_hyperedge_related_task:
            raise ValueError(
                f"Cannot split dataset with task '{to_split.task}' because it is not "
                "a hyperedge-related task. Use 'NodeHDataSplitter' for node-related tasks."
            )

        split_hyperedge_ids_list = kwargs.get("split_hyperedge_ids", [])
        validate_is_non_empty("split_hyperedge_ids", split_hyperedge_ids_list)
        split_hyperedge_ids = torch.as_tensor(
            split_hyperedge_ids_list,
            dtype=torch.long,
            device=to_split.device,
        )

        if is_transductive_setting(self.node_space_setting):
            split_target_hyperedge_mask = torch.zeros(
                to_split.num_hyperedges,
                dtype=torch.bool,
                device=to_split.device,
            )
            split_target_hyperedge_mask[split_hyperedge_ids] = True

            split_hyperedge_attr = (
                to_split.hyperedge_attr.clone() if to_split.hyperedge_attr is not None else None
            )
            split_hyperedge_weights = (
                to_split.hyperedge_weights.clone()
                if to_split.hyperedge_weights is not None
                else None
            )

            return to_split.__class__(
                x=to_split.x.clone(),
                hyperedge_index=to_split.hyperedge_index.clone(),
                hyperedge_weights=split_hyperedge_weights,
                hyperedge_attr=split_hyperedge_attr,
                num_nodes=to_split.num_nodes,
                num_hyperedges=to_split.num_hyperedges,
                global_node_ids=to_split.global_node_ids.clone(),
                target_node_mask=to_split.target_node_mask.clone(),
                target_hyperedge_mask=split_target_hyperedge_mask,
                y=to_split.y.clone(),
                task=to_split.task,
            )

        keep_mask = torch.isin(to_split.hyperedge_index[1], split_hyperedge_ids)
        split_hyperedge_index = to_split.hyperedge_index[:, keep_mask]
        split_unique_hyperedge_ids = split_hyperedge_index[1].unique()

        split_y = to_split.y[split_unique_hyperedge_ids]

        split_hyperedge_attr = (
            to_split.hyperedge_attr[split_unique_hyperedge_ids]
            if to_split.hyperedge_attr is not None
            else None
        )
        split_hyperedge_weights = (
            to_split.hyperedge_weights[split_unique_hyperedge_ids]
            if to_split.hyperedge_weights is not None
            else None
        )
        split_target_hyperedge_mask = torch.ones(
            len(split_unique_hyperedge_ids),
            dtype=torch.bool,
            device=to_split.device,
        )

        split_unique_node_ids = split_hyperedge_index[0].unique()
        split_hyperedge_index = (
            HyperedgeIndex(split_hyperedge_index)
            .to_0based(
                node_ids_to_rebase=split_unique_node_ids,
                hyperedge_ids_to_rebase=split_unique_hyperedge_ids,
            )
            .item
        )

        return to_split.__class__(
            x=to_split.x[split_unique_node_ids],
            hyperedge_index=split_hyperedge_index.clone(),
            hyperedge_weights=split_hyperedge_weights,
            hyperedge_attr=split_hyperedge_attr,
            num_nodes=len(split_unique_node_ids),
            num_hyperedges=len(split_unique_hyperedge_ids),
            global_node_ids=to_split.global_node_ids[split_unique_node_ids],
            target_node_mask=to_split.target_node_mask[split_unique_node_ids],
            target_hyperedge_mask=split_target_hyperedge_mask,
            y=split_y,
            task=to_split.task,
        )

__init__(node_space_setting='transductive')

Initialize the HData splitter.

Parameters:

Name Type Description Default
node_space_setting NodeSpaceSetting

Whether to preserve the full node space in the split.

'transductive'
Source code in hypertorch/data/splitter.py
def __init__(self, node_space_setting: NodeSpaceSetting = "transductive") -> None:
    """
    Initialize the HData splitter.

    Args:
        node_space_setting: Whether to preserve the full node space in the split.
    """
    self.node_space_setting: NodeSpaceSetting = node_space_setting
    validate_node_space_setting(self.node_space_setting)

split(to_split, **kwargs)

Build an HData for a single split from the given hyperedge IDs.

Parameters:

Name Type Description Default
to_split HData

The original HData containing the full hypergraph.

required
kwargs Any

split_hyperedge_ids: The hyperedge IDs that should be included in the split, expected as a keyword argument.

{}

Returns:

Name Type Description
hdata HData

The splitted instance with remapped node and hyperedge IDs.

Source code in hypertorch/data/splitter.py
def split(self, to_split: HData, **kwargs: Any) -> HData:
    """
    Build an HData for a single split from the given hyperedge IDs.

    Args:
        to_split: The original HData containing the full hypergraph.
        kwargs:
            split_hyperedge_ids: The hyperedge IDs that should be included in the split,
                expected as a keyword argument.

    Returns:
        hdata: The splitted instance with remapped node and hyperedge IDs.
    """
    if not to_split.is_hyperedge_related_task:
        raise ValueError(
            f"Cannot split dataset with task '{to_split.task}' because it is not "
            "a hyperedge-related task. Use 'NodeHDataSplitter' for node-related tasks."
        )

    split_hyperedge_ids_list = kwargs.get("split_hyperedge_ids", [])
    validate_is_non_empty("split_hyperedge_ids", split_hyperedge_ids_list)
    split_hyperedge_ids = torch.as_tensor(
        split_hyperedge_ids_list,
        dtype=torch.long,
        device=to_split.device,
    )

    if is_transductive_setting(self.node_space_setting):
        split_target_hyperedge_mask = torch.zeros(
            to_split.num_hyperedges,
            dtype=torch.bool,
            device=to_split.device,
        )
        split_target_hyperedge_mask[split_hyperedge_ids] = True

        split_hyperedge_attr = (
            to_split.hyperedge_attr.clone() if to_split.hyperedge_attr is not None else None
        )
        split_hyperedge_weights = (
            to_split.hyperedge_weights.clone()
            if to_split.hyperedge_weights is not None
            else None
        )

        return to_split.__class__(
            x=to_split.x.clone(),
            hyperedge_index=to_split.hyperedge_index.clone(),
            hyperedge_weights=split_hyperedge_weights,
            hyperedge_attr=split_hyperedge_attr,
            num_nodes=to_split.num_nodes,
            num_hyperedges=to_split.num_hyperedges,
            global_node_ids=to_split.global_node_ids.clone(),
            target_node_mask=to_split.target_node_mask.clone(),
            target_hyperedge_mask=split_target_hyperedge_mask,
            y=to_split.y.clone(),
            task=to_split.task,
        )

    keep_mask = torch.isin(to_split.hyperedge_index[1], split_hyperedge_ids)
    split_hyperedge_index = to_split.hyperedge_index[:, keep_mask]
    split_unique_hyperedge_ids = split_hyperedge_index[1].unique()

    split_y = to_split.y[split_unique_hyperedge_ids]

    split_hyperedge_attr = (
        to_split.hyperedge_attr[split_unique_hyperedge_ids]
        if to_split.hyperedge_attr is not None
        else None
    )
    split_hyperedge_weights = (
        to_split.hyperedge_weights[split_unique_hyperedge_ids]
        if to_split.hyperedge_weights is not None
        else None
    )
    split_target_hyperedge_mask = torch.ones(
        len(split_unique_hyperedge_ids),
        dtype=torch.bool,
        device=to_split.device,
    )

    split_unique_node_ids = split_hyperedge_index[0].unique()
    split_hyperedge_index = (
        HyperedgeIndex(split_hyperedge_index)
        .to_0based(
            node_ids_to_rebase=split_unique_node_ids,
            hyperedge_ids_to_rebase=split_unique_hyperedge_ids,
        )
        .item
    )

    return to_split.__class__(
        x=to_split.x[split_unique_node_ids],
        hyperedge_index=split_hyperedge_index.clone(),
        hyperedge_weights=split_hyperedge_weights,
        hyperedge_attr=split_hyperedge_attr,
        num_nodes=len(split_unique_node_ids),
        num_hyperedges=len(split_unique_hyperedge_ids),
        global_node_ids=to_split.global_node_ids[split_unique_node_ids],
        target_node_mask=to_split.target_node_mask[split_unique_node_ids],
        target_hyperedge_mask=split_target_hyperedge_mask,
        y=split_y,
        task=to_split.task,
    )

HyperedgeIDSplitter

Bases: Splitter['Tensor', tuple[list['Tensor'], list[float]]]

Splitter for hyperedge-ID based dataset partitioning.

Attributes:

Name Type Description
hyperedge_index Tensor

Hypergraph incidence index whose node coverage drives the split logic.

num_nodes int

Number of nodes in the source hypergraph.

num_hyperedges int

Number of hyperedges in the source hypergraph.

device device

Device of the source hyperedge index.

Source code in hypertorch/data/splitter.py
class HyperedgeIDSplitter(Splitter["Tensor", tuple[list["Tensor"], list[float]]]):
    """
    Splitter for hyperedge-ID based dataset partitioning.

    Attributes:
        hyperedge_index: Hypergraph incidence index whose node coverage drives the split logic.
        num_nodes: Number of nodes in the source hypergraph.
        num_hyperedges: Number of hyperedges in the source hypergraph.
        device: Device of the source hyperedge index.
    """

    def __init__(
        self,
        hyperedge_index: Tensor,
        num_nodes: int,
        num_hyperedges: int,
    ) -> None:
        """
        Initialize the hyperedge ID splitter.

        Args:
            hyperedge_index: Hypergraph incidence index whose node coverage drives split logic.
            num_nodes: Number of nodes in the source hypergraph.
            num_hyperedges: Number of hyperedges in the source hypergraph.
        """
        self.hyperedge_index: Tensor = hyperedge_index
        self.num_nodes: int = num_nodes
        self.num_hyperedges: int = num_hyperedges
        self.device: torch.device = hyperedge_index.device

    def ensure_split_covers_all_nodes(
        self,
        hyperedge_ids_by_split: list[Tensor],
        split_idx: int = 0,
    ) -> tuple[list[Tensor], list[float]]:
        """
        Rebalance a split until its hyperedges cover every node in the hypergraph.

        Hyperedges are moved from the other splits into the target split, always
        choosing the donor hyperedge that covers the largest number of currently missing nodes.

        Args:
            hyperedge_ids_by_split: Hyperedge IDs assigned to each split.
            split_idx: Index of the split that must cover the full node space. Defaults to ``0``.

        Returns:
            hyperedge_ids_by_split: The updated hyperedge IDs for each split.
            ratios: The final ratios of hyperedges in each split after rebalancing.

        Raises:
            ValueError: If one or more nodes do not appear in any hyperedge of
                the source hypergraph.
        """
        validate_is_non_empty("hyperedge_ids_by_split", hyperedge_ids_by_split)
        validate_is_between("split_idx", split_idx, 0, len(hyperedge_ids_by_split) - 1)

        required_node_ids = torch.arange(self.num_nodes, dtype=torch.long, device=self.device)
        available_node_ids = self.hyperedge_index[0].unique()
        missing_from_hypergraph_mask = torch.logical_not(
            torch.isin(required_node_ids, available_node_ids)
        )
        if bool(missing_from_hypergraph_mask.any()):
            missing_node_ids = required_node_ids[missing_from_hypergraph_mask].tolist()
            raise ValueError(
                "Cannot create a transductive first split covering all nodes because "
                f"these node ids do not appear in any hyperedge: {missing_node_ids}."
            )

        missing_node_ids = self.__missing_node_ids(
            hyperedge_ids=hyperedge_ids_by_split[split_idx],
            required_node_ids=required_node_ids,
        )
        while missing_node_ids.numel() > 0:
            donor_split_idx, hyperedge_id = self.__pick_covering_hyperedge(
                hyperedge_ids_by_split=hyperedge_ids_by_split,
                missing_node_ids=missing_node_ids,
                split_to_cover_idx=split_idx,
            )
            hyperedge_ids_by_split[split_idx] = torch.cat(
                [hyperedge_ids_by_split[split_idx], hyperedge_id.view(1)]
            )
            donor_hyperedge_ids = hyperedge_ids_by_split[donor_split_idx]
            hyperedge_ids_by_split[donor_split_idx] = donor_hyperedge_ids[
                donor_hyperedge_ids != hyperedge_id
            ]
            missing_node_ids = self.__missing_node_ids(
                hyperedge_ids=hyperedge_ids_by_split[split_idx],
                required_node_ids=required_node_ids,
            )

        return hyperedge_ids_by_split, self.get_split_ratios(hyperedge_ids_by_split)

    def validate_splits_have_hyperedges(self, hyperedge_ids_by_split: list[Tensor]) -> None:
        """
        Validate that every split retains at least one hyperedge.

        Args:
            hyperedge_ids_by_split: Hyperedge IDs assigned to each split.

        Raises:
            ValueError: If any split is empty after splitting or rebalancing.
        """
        empty_split_indices = [
            split_idx
            for split_idx, split_hyperedge_ids in enumerate(hyperedge_ids_by_split)
            if split_hyperedge_ids.numel() == 0
        ]
        if len(empty_split_indices) > 0:
            final_ratios = self.get_split_ratios(hyperedge_ids_by_split)
            raise ValueError(
                f"Splitting produced splits {empty_split_indices} "
                f"with no hyperedges. Final ratios: {final_ratios}."
            )

    def get_hyperedge_ids_permutation(self, shuffle: bool | None, seed: int | None) -> Tensor:
        """
        Return hyperedge IDs in deterministic or shuffled order.

        Args:
            shuffle: Whether to randomly permute the hyperedge IDs.
            seed: Optional random seed used when ``shuffle`` is truthy.

        Returns:
            hyperedge_ids_permutation: Ordered or shuffled hyperedge IDs on the HData device.
        """
        # Shuffle hyperedge IDs if shuffle is requested, otherwise keep original order
        # for deterministic splits
        if shuffle:
            generator = create_seeded_torch_generator(device=self.device, seed=seed)
            random_hyperedge_ids_permutation = torch.randperm(
                n=self.num_hyperedges,
                generator=generator,
                dtype=torch.long,
                device=self.device,
            )
            return random_hyperedge_ids_permutation

        ranged_hyperedge_ids_permutation = torch.arange(
            self.num_hyperedges,
            dtype=torch.long,
            device=self.device,
        )
        return ranged_hyperedge_ids_permutation

    def get_sampleable_hyperedge_ids_permutation(
        self,
        sampleable_hyperedge_ids: Tensor,
        shuffle: bool | None,
        seed: int | None,
    ) -> Tensor:
        """
        Return sampleable hyperedge IDs in deterministic or shuffled order.

        Args:
            sampleable_hyperedge_ids: Hyperedge IDs that can be sampled from the dataset.
            shuffle: Whether to randomly permute the hyperedge IDs.
            seed: Optional random seed used when ``shuffle`` is truthy.

        Returns:
            hyperedge_ids_permutation: Ordered or shuffled hyperedge IDs on the HData device.
        """
        if shuffle:
            generator = create_seeded_torch_generator(device=self.device, seed=seed)
            permutation = torch.randperm(
                n=sampleable_hyperedge_ids.size(0),
                generator=generator,
                dtype=torch.long,
                device=self.device,
            )
            return sampleable_hyperedge_ids[permutation]
        return sampleable_hyperedge_ids.clone()

    def get_split_ratios(self, hyperedge_ids_by_split: list[Tensor]) -> list[float]:
        """
        Compute realized split ratios from hyperedge counts.

        Args:
            hyperedge_ids_by_split: Hyperedge IDs assigned to each split.

        Returns:
            ratios: Ratios derived from the number of hyperedges in each split.
        """
        num_hyperedges_by_split = [
            int(split_hyperedge_ids.numel()) for split_hyperedge_ids in hyperedge_ids_by_split
        ]
        num_hyperedges = sum(num_hyperedges_by_split)
        if num_hyperedges == 0:
            return [0.0 for _ in hyperedge_ids_by_split]

        return [
            round(split_num_hyperedges / num_hyperedges, 4)
            for split_num_hyperedges in num_hyperedges_by_split
        ]

    def split(self, to_split: Tensor, **kwargs: Any) -> tuple[list[Tensor], list[float]]:
        """
        Split hyperedge IDs by cumulative ratio boundaries.

        Early splits use cumulative floor boundaries to avoid over-consuming hyperedges.
        The final split receives any remaining hyperedges caused by rounding.

        Args:
            to_split: Hyperedge IDs to partition.
            kwargs:
                ratios: Desired split ratios, used for initial split construction and
                    as a reference during rebalancing. Expected as a keyword argument.

        Returns:
            hyperedge_ids_by_split: The updated hyperedge IDs for each split.
            ratios: The final ratios of hyperedges in each split after rebalancing.
        """
        ratios: list[float] = kwargs.get("ratios", [])
        validate_ratios(ratios)

        # Cumulative floor boundaries keep early splits from over-consuming hyperedges.
        # The last split absorbs any rounding remainder.
        num_hyperedges = int(to_split.size(0))

        start = 0
        cumulative_ratio = 0.0
        hyperedge_ids_by_split = []
        for split_idx, ratio in enumerate(ratios):
            cumulative_ratio += ratio
            end = (
                num_hyperedges
                if split_idx == len(ratios) - 1
                else int(cumulative_ratio * num_hyperedges)
            )
            hyperedge_ids_by_split.append(to_split[start:end])
            start = end

        return hyperedge_ids_by_split, self.get_split_ratios(hyperedge_ids_by_split)

    def __missing_node_ids(self, hyperedge_ids: Tensor, required_node_ids: Tensor) -> Tensor:
        """
        Return the node IDs not covered by the given hyperedges.

        Args:
            hyperedge_ids: Hyperedge IDs whose node coverage should be inspected.
            required_node_ids: Node IDs that must be covered.

        Returns:
            missing_node_ids: Required node IDs that are still uncovered.
        """
        covered_node_ids = self.__nodes_covered_by_hyperedges(hyperedge_ids)
        covered_node_ids_mask = torch.isin(required_node_ids, covered_node_ids)
        not_covered_node_ids_mask = torch.logical_not(covered_node_ids_mask)
        return required_node_ids[not_covered_node_ids_mask]

    def __nodes_covered_by_hyperedges(self, hyperedge_ids: Tensor) -> Tensor:
        """
        Collect unique node IDs incident to the provided hyperedges.

        Args:
            hyperedge_ids: Hyperedge IDs to inspect.

        Returns:
            nodes_covered_by_hyperedge: Unique node IDs covered by the input hyperedges.
        """
        all_hyperedge_ids = self.hyperedge_index[1]
        nodes_in_input_hyperedges_mask = torch.isin(all_hyperedge_ids, hyperedge_ids)
        all_node_ids = self.hyperedge_index[0]
        return all_node_ids[nodes_in_input_hyperedges_mask].unique()

    def __pick_covering_hyperedge(
        self,
        hyperedge_ids_by_split: list[Tensor],
        missing_node_ids: Tensor,
        split_to_cover_idx: int,
    ) -> tuple[int, Tensor]:
        """
        Choose the donor hyperedge that covers the most currently missing nodes.

        Args:
            hyperedge_ids_by_split: Hyperedge IDs assigned to each split.
            missing_node_ids: Node IDs still missing from the target split.
            split_to_cover_idx: Index of the split being rebalanced.

        Returns:
            split_idx: The index of the donor split containing the selected hyperedge.
            hyperedge_id: The ID of the selected hyperedge.
        """
        best_gain = 0
        best_split_idx: int | None = None
        best_hyperedge_id: Tensor | None = None
        for split_idx, split_hyperedge_ids in enumerate(hyperedge_ids_by_split):
            if split_idx == split_to_cover_idx:
                continue

            for hyperedge_id in split_hyperedge_ids:
                covered_node_ids = self.__nodes_covered_by_hyperedges(hyperedge_id.view(1))
                missing_nodes_in_covered_nodes_mask = torch.isin(covered_node_ids, missing_node_ids)
                gain = missing_nodes_in_covered_nodes_mask.sum(dtype=torch.int).item()
                if gain > best_gain:
                    best_split_idx = split_idx
                    best_hyperedge_id = hyperedge_id
                    best_gain = gain

        # Split construction partitions every available hyperedge, so
        # a valid HData has a covering donor whenever the first split still misses a node
        return cast(int, best_split_idx), cast(Tensor, best_hyperedge_id)

__init__(hyperedge_index, num_nodes, num_hyperedges)

Initialize the hyperedge ID splitter.

Parameters:

Name Type Description Default
hyperedge_index Tensor

Hypergraph incidence index whose node coverage drives split logic.

required
num_nodes int

Number of nodes in the source hypergraph.

required
num_hyperedges int

Number of hyperedges in the source hypergraph.

required
Source code in hypertorch/data/splitter.py
def __init__(
    self,
    hyperedge_index: Tensor,
    num_nodes: int,
    num_hyperedges: int,
) -> None:
    """
    Initialize the hyperedge ID splitter.

    Args:
        hyperedge_index: Hypergraph incidence index whose node coverage drives split logic.
        num_nodes: Number of nodes in the source hypergraph.
        num_hyperedges: Number of hyperedges in the source hypergraph.
    """
    self.hyperedge_index: Tensor = hyperedge_index
    self.num_nodes: int = num_nodes
    self.num_hyperedges: int = num_hyperedges
    self.device: torch.device = hyperedge_index.device

ensure_split_covers_all_nodes(hyperedge_ids_by_split, split_idx=0)

Rebalance a split until its hyperedges cover every node in the hypergraph.

Hyperedges are moved from the other splits into the target split, always choosing the donor hyperedge that covers the largest number of currently missing nodes.

Parameters:

Name Type Description Default
hyperedge_ids_by_split list[Tensor]

Hyperedge IDs assigned to each split.

required
split_idx int

Index of the split that must cover the full node space. Defaults to 0.

0

Returns:

Name Type Description
hyperedge_ids_by_split list[Tensor]

The updated hyperedge IDs for each split.

ratios list[float]

The final ratios of hyperedges in each split after rebalancing.

Raises:

Type Description
ValueError

If one or more nodes do not appear in any hyperedge of the source hypergraph.

Source code in hypertorch/data/splitter.py
def ensure_split_covers_all_nodes(
    self,
    hyperedge_ids_by_split: list[Tensor],
    split_idx: int = 0,
) -> tuple[list[Tensor], list[float]]:
    """
    Rebalance a split until its hyperedges cover every node in the hypergraph.

    Hyperedges are moved from the other splits into the target split, always
    choosing the donor hyperedge that covers the largest number of currently missing nodes.

    Args:
        hyperedge_ids_by_split: Hyperedge IDs assigned to each split.
        split_idx: Index of the split that must cover the full node space. Defaults to ``0``.

    Returns:
        hyperedge_ids_by_split: The updated hyperedge IDs for each split.
        ratios: The final ratios of hyperedges in each split after rebalancing.

    Raises:
        ValueError: If one or more nodes do not appear in any hyperedge of
            the source hypergraph.
    """
    validate_is_non_empty("hyperedge_ids_by_split", hyperedge_ids_by_split)
    validate_is_between("split_idx", split_idx, 0, len(hyperedge_ids_by_split) - 1)

    required_node_ids = torch.arange(self.num_nodes, dtype=torch.long, device=self.device)
    available_node_ids = self.hyperedge_index[0].unique()
    missing_from_hypergraph_mask = torch.logical_not(
        torch.isin(required_node_ids, available_node_ids)
    )
    if bool(missing_from_hypergraph_mask.any()):
        missing_node_ids = required_node_ids[missing_from_hypergraph_mask].tolist()
        raise ValueError(
            "Cannot create a transductive first split covering all nodes because "
            f"these node ids do not appear in any hyperedge: {missing_node_ids}."
        )

    missing_node_ids = self.__missing_node_ids(
        hyperedge_ids=hyperedge_ids_by_split[split_idx],
        required_node_ids=required_node_ids,
    )
    while missing_node_ids.numel() > 0:
        donor_split_idx, hyperedge_id = self.__pick_covering_hyperedge(
            hyperedge_ids_by_split=hyperedge_ids_by_split,
            missing_node_ids=missing_node_ids,
            split_to_cover_idx=split_idx,
        )
        hyperedge_ids_by_split[split_idx] = torch.cat(
            [hyperedge_ids_by_split[split_idx], hyperedge_id.view(1)]
        )
        donor_hyperedge_ids = hyperedge_ids_by_split[donor_split_idx]
        hyperedge_ids_by_split[donor_split_idx] = donor_hyperedge_ids[
            donor_hyperedge_ids != hyperedge_id
        ]
        missing_node_ids = self.__missing_node_ids(
            hyperedge_ids=hyperedge_ids_by_split[split_idx],
            required_node_ids=required_node_ids,
        )

    return hyperedge_ids_by_split, self.get_split_ratios(hyperedge_ids_by_split)

validate_splits_have_hyperedges(hyperedge_ids_by_split)

Validate that every split retains at least one hyperedge.

Parameters:

Name Type Description Default
hyperedge_ids_by_split list[Tensor]

Hyperedge IDs assigned to each split.

required

Raises:

Type Description
ValueError

If any split is empty after splitting or rebalancing.

Source code in hypertorch/data/splitter.py
def validate_splits_have_hyperedges(self, hyperedge_ids_by_split: list[Tensor]) -> None:
    """
    Validate that every split retains at least one hyperedge.

    Args:
        hyperedge_ids_by_split: Hyperedge IDs assigned to each split.

    Raises:
        ValueError: If any split is empty after splitting or rebalancing.
    """
    empty_split_indices = [
        split_idx
        for split_idx, split_hyperedge_ids in enumerate(hyperedge_ids_by_split)
        if split_hyperedge_ids.numel() == 0
    ]
    if len(empty_split_indices) > 0:
        final_ratios = self.get_split_ratios(hyperedge_ids_by_split)
        raise ValueError(
            f"Splitting produced splits {empty_split_indices} "
            f"with no hyperedges. Final ratios: {final_ratios}."
        )

get_hyperedge_ids_permutation(shuffle, seed)

Return hyperedge IDs in deterministic or shuffled order.

Parameters:

Name Type Description Default
shuffle bool | None

Whether to randomly permute the hyperedge IDs.

required
seed int | None

Optional random seed used when shuffle is truthy.

required

Returns:

Name Type Description
hyperedge_ids_permutation Tensor

Ordered or shuffled hyperedge IDs on the HData device.

Source code in hypertorch/data/splitter.py
def get_hyperedge_ids_permutation(self, shuffle: bool | None, seed: int | None) -> Tensor:
    """
    Return hyperedge IDs in deterministic or shuffled order.

    Args:
        shuffle: Whether to randomly permute the hyperedge IDs.
        seed: Optional random seed used when ``shuffle`` is truthy.

    Returns:
        hyperedge_ids_permutation: Ordered or shuffled hyperedge IDs on the HData device.
    """
    # Shuffle hyperedge IDs if shuffle is requested, otherwise keep original order
    # for deterministic splits
    if shuffle:
        generator = create_seeded_torch_generator(device=self.device, seed=seed)
        random_hyperedge_ids_permutation = torch.randperm(
            n=self.num_hyperedges,
            generator=generator,
            dtype=torch.long,
            device=self.device,
        )
        return random_hyperedge_ids_permutation

    ranged_hyperedge_ids_permutation = torch.arange(
        self.num_hyperedges,
        dtype=torch.long,
        device=self.device,
    )
    return ranged_hyperedge_ids_permutation

get_sampleable_hyperedge_ids_permutation(sampleable_hyperedge_ids, shuffle, seed)

Return sampleable hyperedge IDs in deterministic or shuffled order.

Parameters:

Name Type Description Default
sampleable_hyperedge_ids Tensor

Hyperedge IDs that can be sampled from the dataset.

required
shuffle bool | None

Whether to randomly permute the hyperedge IDs.

required
seed int | None

Optional random seed used when shuffle is truthy.

required

Returns:

Name Type Description
hyperedge_ids_permutation Tensor

Ordered or shuffled hyperedge IDs on the HData device.

Source code in hypertorch/data/splitter.py
def get_sampleable_hyperedge_ids_permutation(
    self,
    sampleable_hyperedge_ids: Tensor,
    shuffle: bool | None,
    seed: int | None,
) -> Tensor:
    """
    Return sampleable hyperedge IDs in deterministic or shuffled order.

    Args:
        sampleable_hyperedge_ids: Hyperedge IDs that can be sampled from the dataset.
        shuffle: Whether to randomly permute the hyperedge IDs.
        seed: Optional random seed used when ``shuffle`` is truthy.

    Returns:
        hyperedge_ids_permutation: Ordered or shuffled hyperedge IDs on the HData device.
    """
    if shuffle:
        generator = create_seeded_torch_generator(device=self.device, seed=seed)
        permutation = torch.randperm(
            n=sampleable_hyperedge_ids.size(0),
            generator=generator,
            dtype=torch.long,
            device=self.device,
        )
        return sampleable_hyperedge_ids[permutation]
    return sampleable_hyperedge_ids.clone()

get_split_ratios(hyperedge_ids_by_split)

Compute realized split ratios from hyperedge counts.

Parameters:

Name Type Description Default
hyperedge_ids_by_split list[Tensor]

Hyperedge IDs assigned to each split.

required

Returns:

Name Type Description
ratios list[float]

Ratios derived from the number of hyperedges in each split.

Source code in hypertorch/data/splitter.py
def get_split_ratios(self, hyperedge_ids_by_split: list[Tensor]) -> list[float]:
    """
    Compute realized split ratios from hyperedge counts.

    Args:
        hyperedge_ids_by_split: Hyperedge IDs assigned to each split.

    Returns:
        ratios: Ratios derived from the number of hyperedges in each split.
    """
    num_hyperedges_by_split = [
        int(split_hyperedge_ids.numel()) for split_hyperedge_ids in hyperedge_ids_by_split
    ]
    num_hyperedges = sum(num_hyperedges_by_split)
    if num_hyperedges == 0:
        return [0.0 for _ in hyperedge_ids_by_split]

    return [
        round(split_num_hyperedges / num_hyperedges, 4)
        for split_num_hyperedges in num_hyperedges_by_split
    ]

split(to_split, **kwargs)

Split hyperedge IDs by cumulative ratio boundaries.

Early splits use cumulative floor boundaries to avoid over-consuming hyperedges. The final split receives any remaining hyperedges caused by rounding.

Parameters:

Name Type Description Default
to_split Tensor

Hyperedge IDs to partition.

required
kwargs Any

ratios: Desired split ratios, used for initial split construction and as a reference during rebalancing. Expected as a keyword argument.

{}

Returns:

Name Type Description
hyperedge_ids_by_split list[Tensor]

The updated hyperedge IDs for each split.

ratios list[float]

The final ratios of hyperedges in each split after rebalancing.

Source code in hypertorch/data/splitter.py
def split(self, to_split: Tensor, **kwargs: Any) -> tuple[list[Tensor], list[float]]:
    """
    Split hyperedge IDs by cumulative ratio boundaries.

    Early splits use cumulative floor boundaries to avoid over-consuming hyperedges.
    The final split receives any remaining hyperedges caused by rounding.

    Args:
        to_split: Hyperedge IDs to partition.
        kwargs:
            ratios: Desired split ratios, used for initial split construction and
                as a reference during rebalancing. Expected as a keyword argument.

    Returns:
        hyperedge_ids_by_split: The updated hyperedge IDs for each split.
        ratios: The final ratios of hyperedges in each split after rebalancing.
    """
    ratios: list[float] = kwargs.get("ratios", [])
    validate_ratios(ratios)

    # Cumulative floor boundaries keep early splits from over-consuming hyperedges.
    # The last split absorbs any rounding remainder.
    num_hyperedges = int(to_split.size(0))

    start = 0
    cumulative_ratio = 0.0
    hyperedge_ids_by_split = []
    for split_idx, ratio in enumerate(ratios):
        cumulative_ratio += ratio
        end = (
            num_hyperedges
            if split_idx == len(ratios) - 1
            else int(cumulative_ratio * num_hyperedges)
        )
        hyperedge_ids_by_split.append(to_split[start:end])
        start = end

    return hyperedge_ids_by_split, self.get_split_ratios(hyperedge_ids_by_split)

__missing_node_ids(hyperedge_ids, required_node_ids)

Return the node IDs not covered by the given hyperedges.

Parameters:

Name Type Description Default
hyperedge_ids Tensor

Hyperedge IDs whose node coverage should be inspected.

required
required_node_ids Tensor

Node IDs that must be covered.

required

Returns:

Name Type Description
missing_node_ids Tensor

Required node IDs that are still uncovered.

Source code in hypertorch/data/splitter.py
def __missing_node_ids(self, hyperedge_ids: Tensor, required_node_ids: Tensor) -> Tensor:
    """
    Return the node IDs not covered by the given hyperedges.

    Args:
        hyperedge_ids: Hyperedge IDs whose node coverage should be inspected.
        required_node_ids: Node IDs that must be covered.

    Returns:
        missing_node_ids: Required node IDs that are still uncovered.
    """
    covered_node_ids = self.__nodes_covered_by_hyperedges(hyperedge_ids)
    covered_node_ids_mask = torch.isin(required_node_ids, covered_node_ids)
    not_covered_node_ids_mask = torch.logical_not(covered_node_ids_mask)
    return required_node_ids[not_covered_node_ids_mask]

__nodes_covered_by_hyperedges(hyperedge_ids)

Collect unique node IDs incident to the provided hyperedges.

Parameters:

Name Type Description Default
hyperedge_ids Tensor

Hyperedge IDs to inspect.

required

Returns:

Name Type Description
nodes_covered_by_hyperedge Tensor

Unique node IDs covered by the input hyperedges.

Source code in hypertorch/data/splitter.py
def __nodes_covered_by_hyperedges(self, hyperedge_ids: Tensor) -> Tensor:
    """
    Collect unique node IDs incident to the provided hyperedges.

    Args:
        hyperedge_ids: Hyperedge IDs to inspect.

    Returns:
        nodes_covered_by_hyperedge: Unique node IDs covered by the input hyperedges.
    """
    all_hyperedge_ids = self.hyperedge_index[1]
    nodes_in_input_hyperedges_mask = torch.isin(all_hyperedge_ids, hyperedge_ids)
    all_node_ids = self.hyperedge_index[0]
    return all_node_ids[nodes_in_input_hyperedges_mask].unique()

__pick_covering_hyperedge(hyperedge_ids_by_split, missing_node_ids, split_to_cover_idx)

Choose the donor hyperedge that covers the most currently missing nodes.

Parameters:

Name Type Description Default
hyperedge_ids_by_split list[Tensor]

Hyperedge IDs assigned to each split.

required
missing_node_ids Tensor

Node IDs still missing from the target split.

required
split_to_cover_idx int

Index of the split being rebalanced.

required

Returns:

Name Type Description
split_idx int

The index of the donor split containing the selected hyperedge.

hyperedge_id Tensor

The ID of the selected hyperedge.

Source code in hypertorch/data/splitter.py
def __pick_covering_hyperedge(
    self,
    hyperedge_ids_by_split: list[Tensor],
    missing_node_ids: Tensor,
    split_to_cover_idx: int,
) -> tuple[int, Tensor]:
    """
    Choose the donor hyperedge that covers the most currently missing nodes.

    Args:
        hyperedge_ids_by_split: Hyperedge IDs assigned to each split.
        missing_node_ids: Node IDs still missing from the target split.
        split_to_cover_idx: Index of the split being rebalanced.

    Returns:
        split_idx: The index of the donor split containing the selected hyperedge.
        hyperedge_id: The ID of the selected hyperedge.
    """
    best_gain = 0
    best_split_idx: int | None = None
    best_hyperedge_id: Tensor | None = None
    for split_idx, split_hyperedge_ids in enumerate(hyperedge_ids_by_split):
        if split_idx == split_to_cover_idx:
            continue

        for hyperedge_id in split_hyperedge_ids:
            covered_node_ids = self.__nodes_covered_by_hyperedges(hyperedge_id.view(1))
            missing_nodes_in_covered_nodes_mask = torch.isin(covered_node_ids, missing_node_ids)
            gain = missing_nodes_in_covered_nodes_mask.sum(dtype=torch.int).item()
            if gain > best_gain:
                best_split_idx = split_idx
                best_hyperedge_id = hyperedge_id
                best_gain = gain

    # Split construction partitions every available hyperedge, so
    # a valid HData has a covering donor whenever the first split still misses a node
    return cast(int, best_split_idx), cast(Tensor, best_hyperedge_id)

NodeDatasetSplitter

Bases: Splitter['Dataset', tuple[list['Dataset'], list[float]]]

Split a node-classification dataset by supervised node IDs.

Attributes:

Name Type Description
node_space_setting NodeSpaceSetting

Whether to preserve the full graph in every split.

shuffle bool | None

Whether to shuffle node IDs before splitting.

seed int | None

Random seed used when shuffling.

Source code in hypertorch/data/splitter.py
class NodeDatasetSplitter(Splitter["Dataset", tuple[list["Dataset"], list[float]]]):
    """
    Split a node-classification dataset by supervised node IDs.

    Attributes:
        node_space_setting: Whether to preserve the full graph in every split.
        shuffle: Whether to shuffle node IDs before splitting.
        seed: Random seed used when shuffling.
    """

    def __init__(
        self,
        node_space_setting: NodeSpaceSetting = "transductive",
        shuffle: bool | None = False,
        seed: int | None = None,
    ) -> None:
        """
        Initialize the node dataset splitter.

        Args:
            node_space_setting: Whether to preserve the full graph in every split.
            shuffle: Whether to shuffle node IDs before splitting.
            seed: Optional random seed for reproducibility.
        """
        self.node_space_setting: NodeSpaceSetting = node_space_setting
        self.shuffle: bool | None = shuffle
        self.seed: int | None = seed
        validate_node_space_setting(self.node_space_setting)

    def split(self, to_split: Dataset, **kwargs: Any) -> tuple[list[Dataset], list[float]]:
        """
        Split a dataset and return materialized node-classification partitions.

        Args:
            to_split: The dataset to split.
            kwargs:
                ratios: Desired split ratios, used for initial split construction and
                    as a reference during rebalancing. Expected as a keyword argument.

        Returns:
            split_datasets: The list of split datasets.
        """
        if not to_split.hdata.is_node_related_task:
            raise ValueError(
                f"Cannot split dataset with task '{to_split.task}' because it is not "
                "a node-related task. Use 'HyperedgeDatasetSplitter or "
                "'SparseHyperedgeDatasetSplitter' for hyperedge-related tasks."
            )

        ratios: list[float] = kwargs.get("ratios", [])
        validate_ratios(ratios)

        hdata = to_split.hdata
        node_splitter = NodeIDSplitter(num_nodes=hdata.num_nodes, device=hdata.device)
        node_ids_permutation = node_splitter.get_sampleable_node_ids_permutation(
            sampleable_node_ids=hdata.sampleable_node_ids,
            shuffle=self.shuffle,
            seed=self.seed,
        )
        node_ids_by_split, final_ratios = node_splitter.split(
            to_split=node_ids_permutation,
            ratios=ratios,
        )
        node_splitter.validate_splits_have_nodes(node_ids_by_split)

        split_datasets: list[Dataset] = []
        for split_node_ids in node_ids_by_split:
            split_hdata = NodeHDataSplitter(self.node_space_setting).split(
                to_split=hdata,
                split_node_ids=split_node_ids,
            )
            split_hdata = split_hdata.to(device=hdata.device)
            split_dataset = to_split.__class__(
                hdata=split_hdata,
                sampling_strategy=to_split.sampling_strategy,
            )
            split_datasets.append(split_dataset)

        return split_datasets, final_ratios

__init__(node_space_setting='transductive', shuffle=False, seed=None)

Initialize the node dataset splitter.

Parameters:

Name Type Description Default
node_space_setting NodeSpaceSetting

Whether to preserve the full graph in every split.

'transductive'
shuffle bool | None

Whether to shuffle node IDs before splitting.

False
seed int | None

Optional random seed for reproducibility.

None
Source code in hypertorch/data/splitter.py
def __init__(
    self,
    node_space_setting: NodeSpaceSetting = "transductive",
    shuffle: bool | None = False,
    seed: int | None = None,
) -> None:
    """
    Initialize the node dataset splitter.

    Args:
        node_space_setting: Whether to preserve the full graph in every split.
        shuffle: Whether to shuffle node IDs before splitting.
        seed: Optional random seed for reproducibility.
    """
    self.node_space_setting: NodeSpaceSetting = node_space_setting
    self.shuffle: bool | None = shuffle
    self.seed: int | None = seed
    validate_node_space_setting(self.node_space_setting)

split(to_split, **kwargs)

Split a dataset and return materialized node-classification partitions.

Parameters:

Name Type Description Default
to_split Dataset

The dataset to split.

required
kwargs Any

ratios: Desired split ratios, used for initial split construction and as a reference during rebalancing. Expected as a keyword argument.

{}

Returns:

Name Type Description
split_datasets tuple[list[Dataset], list[float]]

The list of split datasets.

Source code in hypertorch/data/splitter.py
def split(self, to_split: Dataset, **kwargs: Any) -> tuple[list[Dataset], list[float]]:
    """
    Split a dataset and return materialized node-classification partitions.

    Args:
        to_split: The dataset to split.
        kwargs:
            ratios: Desired split ratios, used for initial split construction and
                as a reference during rebalancing. Expected as a keyword argument.

    Returns:
        split_datasets: The list of split datasets.
    """
    if not to_split.hdata.is_node_related_task:
        raise ValueError(
            f"Cannot split dataset with task '{to_split.task}' because it is not "
            "a node-related task. Use 'HyperedgeDatasetSplitter or "
            "'SparseHyperedgeDatasetSplitter' for hyperedge-related tasks."
        )

    ratios: list[float] = kwargs.get("ratios", [])
    validate_ratios(ratios)

    hdata = to_split.hdata
    node_splitter = NodeIDSplitter(num_nodes=hdata.num_nodes, device=hdata.device)
    node_ids_permutation = node_splitter.get_sampleable_node_ids_permutation(
        sampleable_node_ids=hdata.sampleable_node_ids,
        shuffle=self.shuffle,
        seed=self.seed,
    )
    node_ids_by_split, final_ratios = node_splitter.split(
        to_split=node_ids_permutation,
        ratios=ratios,
    )
    node_splitter.validate_splits_have_nodes(node_ids_by_split)

    split_datasets: list[Dataset] = []
    for split_node_ids in node_ids_by_split:
        split_hdata = NodeHDataSplitter(self.node_space_setting).split(
            to_split=hdata,
            split_node_ids=split_node_ids,
        )
        split_hdata = split_hdata.to(device=hdata.device)
        split_dataset = to_split.__class__(
            hdata=split_hdata,
            sampling_strategy=to_split.sampling_strategy,
        )
        split_datasets.append(split_dataset)

    return split_datasets, final_ratios

NodeHDataSplitter

Bases: Splitter['HData', 'HData']

Materialize an HData split from explicit node IDs.

Attributes:

Name Type Description
node_space_setting NodeSpaceSetting

Whether to preserve the full node space in the split.

Source code in hypertorch/data/splitter.py
class NodeHDataSplitter(Splitter["HData", "HData"]):
    """
    Materialize an `HData` split from explicit node IDs.

    Attributes:
        node_space_setting: Whether to preserve the full node space in the split.
    """

    def __init__(self, node_space_setting: NodeSpaceSetting = "transductive") -> None:
        """
        Initialize the node HData splitter.

        Args:
            node_space_setting: Whether to preserve the full node space in the split.
        """
        self.node_space_setting: NodeSpaceSetting = node_space_setting
        validate_node_space_setting(self.node_space_setting)

    def split(self, to_split: HData, **kwargs: Any) -> HData:
        """
        Build an HData for a single split from the given node IDs.

        Args:
            to_split: The original `HData` containing the full hypergraph.
            kwargs:
                split_node_ids: The node IDs that should be included in the split,
                    expected as a keyword argument.

        Returns:
            hdata: The splitted instance with remapped node and hyperedge IDs.
        """
        if not to_split.is_node_related_task:
            raise ValueError(
                f"Cannot split dataset with task '{to_split.task}' because it is not "
                "a node-related task. Use 'HyperedgeHDataSplitter or "
                "'SparseHyperedgeHDataSplitter' for hyperedge-related tasks."
            )

        split_node_ids = kwargs.get("split_node_ids", [])
        validate_is_non_empty("split_node_ids", split_node_ids)

        if is_transductive_setting(self.node_space_setting):
            split_target_node_mask = torch.zeros(
                to_split.num_nodes,
                dtype=torch.bool,
                device=to_split.device,
            )
            split_target_node_mask[split_node_ids] = True

            split_hyperedge_attr = (
                to_split.hyperedge_attr.clone() if to_split.hyperedge_attr is not None else None
            )
            split_hyperedge_weights = (
                to_split.hyperedge_weights.clone()
                if to_split.hyperedge_weights is not None
                else None
            )

            return to_split.__class__(
                x=to_split.x.clone(),
                hyperedge_index=to_split.hyperedge_index.clone(),
                hyperedge_weights=split_hyperedge_weights,
                hyperedge_attr=split_hyperedge_attr,
                num_nodes=to_split.num_nodes,
                num_hyperedges=to_split.num_hyperedges,
                global_node_ids=to_split.global_node_ids.clone(),
                target_node_mask=split_target_node_mask,
                target_hyperedge_mask=to_split.target_hyperedge_mask.clone(),
                y=to_split.y.clone(),
                task=to_split.task,
            )

        keep_mask = torch.isin(to_split.hyperedge_index[0], split_node_ids)
        split_hyperedge_index = to_split.hyperedge_index[:, keep_mask]
        split_unique_hyperedge_ids = split_hyperedge_index[1].unique(sorted=True)

        split_hyperedge_attr = (
            to_split.hyperedge_attr[split_unique_hyperedge_ids]
            if to_split.hyperedge_attr is not None
            else None
        )
        split_hyperedge_weights = (
            to_split.hyperedge_weights[split_unique_hyperedge_ids]
            if to_split.hyperedge_weights is not None
            else None
        )

        split_unique_node_ids = split_hyperedge_index[0].unique(sorted=True)
        split_hyperedge_index = (
            HyperedgeIndex(split_hyperedge_index)
            .to_0based(
                node_ids_to_rebase=split_unique_node_ids,
                hyperedge_ids_to_rebase=split_unique_hyperedge_ids,
            )
            .item
        )

        split_target_node_mask = torch.ones(
            len(split_unique_node_ids),
            dtype=torch.bool,
            device=to_split.device,
        )
        return to_split.__class__(
            x=to_split.x[split_unique_node_ids],
            hyperedge_index=split_hyperedge_index.clone(),
            hyperedge_weights=split_hyperedge_weights,
            hyperedge_attr=split_hyperedge_attr,
            num_nodes=len(split_unique_node_ids),
            num_hyperedges=len(split_unique_hyperedge_ids),
            global_node_ids=to_split.global_node_ids[split_unique_node_ids],
            y=to_split.y[split_unique_node_ids],
            task=to_split.task,
            target_node_mask=split_target_node_mask,
            target_hyperedge_mask=to_split.target_hyperedge_mask[split_unique_hyperedge_ids],
        )

__init__(node_space_setting='transductive')

Initialize the node HData splitter.

Parameters:

Name Type Description Default
node_space_setting NodeSpaceSetting

Whether to preserve the full node space in the split.

'transductive'
Source code in hypertorch/data/splitter.py
def __init__(self, node_space_setting: NodeSpaceSetting = "transductive") -> None:
    """
    Initialize the node HData splitter.

    Args:
        node_space_setting: Whether to preserve the full node space in the split.
    """
    self.node_space_setting: NodeSpaceSetting = node_space_setting
    validate_node_space_setting(self.node_space_setting)

split(to_split, **kwargs)

Build an HData for a single split from the given node IDs.

Parameters:

Name Type Description Default
to_split HData

The original HData containing the full hypergraph.

required
kwargs Any

split_node_ids: The node IDs that should be included in the split, expected as a keyword argument.

{}

Returns:

Name Type Description
hdata HData

The splitted instance with remapped node and hyperedge IDs.

Source code in hypertorch/data/splitter.py
def split(self, to_split: HData, **kwargs: Any) -> HData:
    """
    Build an HData for a single split from the given node IDs.

    Args:
        to_split: The original `HData` containing the full hypergraph.
        kwargs:
            split_node_ids: The node IDs that should be included in the split,
                expected as a keyword argument.

    Returns:
        hdata: The splitted instance with remapped node and hyperedge IDs.
    """
    if not to_split.is_node_related_task:
        raise ValueError(
            f"Cannot split dataset with task '{to_split.task}' because it is not "
            "a node-related task. Use 'HyperedgeHDataSplitter or "
            "'SparseHyperedgeHDataSplitter' for hyperedge-related tasks."
        )

    split_node_ids = kwargs.get("split_node_ids", [])
    validate_is_non_empty("split_node_ids", split_node_ids)

    if is_transductive_setting(self.node_space_setting):
        split_target_node_mask = torch.zeros(
            to_split.num_nodes,
            dtype=torch.bool,
            device=to_split.device,
        )
        split_target_node_mask[split_node_ids] = True

        split_hyperedge_attr = (
            to_split.hyperedge_attr.clone() if to_split.hyperedge_attr is not None else None
        )
        split_hyperedge_weights = (
            to_split.hyperedge_weights.clone()
            if to_split.hyperedge_weights is not None
            else None
        )

        return to_split.__class__(
            x=to_split.x.clone(),
            hyperedge_index=to_split.hyperedge_index.clone(),
            hyperedge_weights=split_hyperedge_weights,
            hyperedge_attr=split_hyperedge_attr,
            num_nodes=to_split.num_nodes,
            num_hyperedges=to_split.num_hyperedges,
            global_node_ids=to_split.global_node_ids.clone(),
            target_node_mask=split_target_node_mask,
            target_hyperedge_mask=to_split.target_hyperedge_mask.clone(),
            y=to_split.y.clone(),
            task=to_split.task,
        )

    keep_mask = torch.isin(to_split.hyperedge_index[0], split_node_ids)
    split_hyperedge_index = to_split.hyperedge_index[:, keep_mask]
    split_unique_hyperedge_ids = split_hyperedge_index[1].unique(sorted=True)

    split_hyperedge_attr = (
        to_split.hyperedge_attr[split_unique_hyperedge_ids]
        if to_split.hyperedge_attr is not None
        else None
    )
    split_hyperedge_weights = (
        to_split.hyperedge_weights[split_unique_hyperedge_ids]
        if to_split.hyperedge_weights is not None
        else None
    )

    split_unique_node_ids = split_hyperedge_index[0].unique(sorted=True)
    split_hyperedge_index = (
        HyperedgeIndex(split_hyperedge_index)
        .to_0based(
            node_ids_to_rebase=split_unique_node_ids,
            hyperedge_ids_to_rebase=split_unique_hyperedge_ids,
        )
        .item
    )

    split_target_node_mask = torch.ones(
        len(split_unique_node_ids),
        dtype=torch.bool,
        device=to_split.device,
    )
    return to_split.__class__(
        x=to_split.x[split_unique_node_ids],
        hyperedge_index=split_hyperedge_index.clone(),
        hyperedge_weights=split_hyperedge_weights,
        hyperedge_attr=split_hyperedge_attr,
        num_nodes=len(split_unique_node_ids),
        num_hyperedges=len(split_unique_hyperedge_ids),
        global_node_ids=to_split.global_node_ids[split_unique_node_ids],
        y=to_split.y[split_unique_node_ids],
        task=to_split.task,
        target_node_mask=split_target_node_mask,
        target_hyperedge_mask=to_split.target_hyperedge_mask[split_unique_hyperedge_ids],
    )

NodeIDSplitter

Bases: Splitter['Tensor', tuple[list['Tensor'], list[float]]]

Splitter for node-ID based dataset partitioning.

Attributes:

Name Type Description
num_nodes int

Number of nodes in the source hypergraph.

device device

Device for generated node ID tensors.

Source code in hypertorch/data/splitter.py
class NodeIDSplitter(Splitter["Tensor", tuple[list["Tensor"], list[float]]]):
    """
    Splitter for node-ID based dataset partitioning.

    Attributes:
        num_nodes: Number of nodes in the source hypergraph.
        device: Device for generated node ID tensors.
    """

    def __init__(self, num_nodes: int, device: torch.device) -> None:
        """
        Initialize the node ID splitter.

        Args:
            num_nodes: Number of nodes in the source hypergraph.
            device: Device for generated node ID tensors.
        """
        self.num_nodes: int = num_nodes
        self.device: torch.device = device

    def get_node_ids_permutation(self, shuffle: bool | None, seed: int | None) -> Tensor:
        """
        Return node IDs in deterministic or shuffled order.

        Args:
            shuffle: Whether to randomly permute the node IDs.
            seed: Optional random seed used when ``shuffle`` is truthy.

        Returns:
            node_ids_permutation: Ordered or shuffled node IDs on the HData device.
        """
        if shuffle:
            generator = create_seeded_torch_generator(device=self.device, seed=seed)
            return torch.randperm(
                n=self.num_nodes,
                generator=generator,
                dtype=torch.long,
                device=self.device,
            )

        return torch.arange(self.num_nodes, dtype=torch.long, device=self.device)

    def get_sampleable_node_ids_permutation(
        self,
        sampleable_node_ids: Tensor,
        shuffle: bool | None,
        seed: int | None,
    ) -> Tensor:
        """
        Return sampleable node IDs in deterministic or shuffled order.

        Args:
            shuffle: Whether to randomly permute the node IDs.
            seed: Optional random seed used when ``shuffle`` is truthy.

        Returns:
            node_ids_permutation: Ordered or shuffled node IDs on the HData device.
        """
        if shuffle:
            generator = create_seeded_torch_generator(device=self.device, seed=seed)
            permutation = torch.randperm(
                n=sampleable_node_ids.size(0),
                generator=generator,
                dtype=torch.long,
                device=self.device,
            )
            return sampleable_node_ids[permutation]
        return sampleable_node_ids.clone()

    def validate_splits_have_nodes(self, node_ids_by_split: list[Tensor]) -> None:
        """
        Validate that every split has at least one node.

        Args:
            node_ids_by_split: Node IDs assigned to each split.

        Raises:
            ValueError: If any split is empty after splitting.
        """
        empty_split_indices = [
            split_idx
            for split_idx, split_node_ids in enumerate(node_ids_by_split)
            if split_node_ids.numel() == 0
        ]
        if len(empty_split_indices) > 0:
            final_ratios = self.get_split_ratios(node_ids_by_split)
            raise ValueError(
                f"Splitting produced splits {empty_split_indices} "
                f"with no nodes. Final ratios: {final_ratios}."
            )

    def get_split_ratios(self, node_ids_by_split: list[Tensor]) -> list[float]:
        """
        Compute realized split ratios from node counts.

        Args:
            node_ids_by_split: Node IDs assigned to each split.

        Returns:
            ratios: Ratios derived from the number of nodes in each split.
        """
        num_nodes_by_split = [int(split_node_ids.numel()) for split_node_ids in node_ids_by_split]
        num_nodes = sum(num_nodes_by_split)
        if num_nodes == 0:
            return [0.0 for _ in node_ids_by_split]

        return [round(split_num_nodes / num_nodes, 4) for split_num_nodes in num_nodes_by_split]

    def split(self, to_split: Tensor, **kwargs: Any) -> tuple[list[Tensor], list[float]]:
        """
        Split node IDs by cumulative ratio boundaries.

        Early splits use cumulative floor boundaries to avoid over-consuming nodes.
        The final split receives any remaining nodes caused by rounding.

        Args:
            to_split: Node IDs to partition.
            kwargs:
                ratios: Desired split ratios, used for initial split construction and
                    as a reference during rebalancing. Expected as a keyword argument.

        Returns:
            node_ids_by_split: The updated node IDs for each split.
            ratios: The final split ratios.
        """
        ratios: list[float] = kwargs.get("ratios", [])
        validate_ratios(ratios)

        # Cumulative floor boundaries keep early splits from over-consuming nodes.
        # The last split absorbs any rounding remainder.
        num_nodes = int(to_split.size(0))

        start = 0
        cumulative_ratio = 0.0
        node_ids_by_split = []
        for split_idx, ratio in enumerate(ratios):
            cumulative_ratio += ratio
            end = num_nodes if split_idx == len(ratios) - 1 else int(cumulative_ratio * num_nodes)
            node_ids_by_split.append(to_split[start:end])
            start = end

        return node_ids_by_split, self.get_split_ratios(node_ids_by_split)

__init__(num_nodes, device)

Initialize the node ID splitter.

Parameters:

Name Type Description Default
num_nodes int

Number of nodes in the source hypergraph.

required
device device

Device for generated node ID tensors.

required
Source code in hypertorch/data/splitter.py
def __init__(self, num_nodes: int, device: torch.device) -> None:
    """
    Initialize the node ID splitter.

    Args:
        num_nodes: Number of nodes in the source hypergraph.
        device: Device for generated node ID tensors.
    """
    self.num_nodes: int = num_nodes
    self.device: torch.device = device

get_node_ids_permutation(shuffle, seed)

Return node IDs in deterministic or shuffled order.

Parameters:

Name Type Description Default
shuffle bool | None

Whether to randomly permute the node IDs.

required
seed int | None

Optional random seed used when shuffle is truthy.

required

Returns:

Name Type Description
node_ids_permutation Tensor

Ordered or shuffled node IDs on the HData device.

Source code in hypertorch/data/splitter.py
def get_node_ids_permutation(self, shuffle: bool | None, seed: int | None) -> Tensor:
    """
    Return node IDs in deterministic or shuffled order.

    Args:
        shuffle: Whether to randomly permute the node IDs.
        seed: Optional random seed used when ``shuffle`` is truthy.

    Returns:
        node_ids_permutation: Ordered or shuffled node IDs on the HData device.
    """
    if shuffle:
        generator = create_seeded_torch_generator(device=self.device, seed=seed)
        return torch.randperm(
            n=self.num_nodes,
            generator=generator,
            dtype=torch.long,
            device=self.device,
        )

    return torch.arange(self.num_nodes, dtype=torch.long, device=self.device)

get_sampleable_node_ids_permutation(sampleable_node_ids, shuffle, seed)

Return sampleable node IDs in deterministic or shuffled order.

Parameters:

Name Type Description Default
shuffle bool | None

Whether to randomly permute the node IDs.

required
seed int | None

Optional random seed used when shuffle is truthy.

required

Returns:

Name Type Description
node_ids_permutation Tensor

Ordered or shuffled node IDs on the HData device.

Source code in hypertorch/data/splitter.py
def get_sampleable_node_ids_permutation(
    self,
    sampleable_node_ids: Tensor,
    shuffle: bool | None,
    seed: int | None,
) -> Tensor:
    """
    Return sampleable node IDs in deterministic or shuffled order.

    Args:
        shuffle: Whether to randomly permute the node IDs.
        seed: Optional random seed used when ``shuffle`` is truthy.

    Returns:
        node_ids_permutation: Ordered or shuffled node IDs on the HData device.
    """
    if shuffle:
        generator = create_seeded_torch_generator(device=self.device, seed=seed)
        permutation = torch.randperm(
            n=sampleable_node_ids.size(0),
            generator=generator,
            dtype=torch.long,
            device=self.device,
        )
        return sampleable_node_ids[permutation]
    return sampleable_node_ids.clone()

validate_splits_have_nodes(node_ids_by_split)

Validate that every split has at least one node.

Parameters:

Name Type Description Default
node_ids_by_split list[Tensor]

Node IDs assigned to each split.

required

Raises:

Type Description
ValueError

If any split is empty after splitting.

Source code in hypertorch/data/splitter.py
def validate_splits_have_nodes(self, node_ids_by_split: list[Tensor]) -> None:
    """
    Validate that every split has at least one node.

    Args:
        node_ids_by_split: Node IDs assigned to each split.

    Raises:
        ValueError: If any split is empty after splitting.
    """
    empty_split_indices = [
        split_idx
        for split_idx, split_node_ids in enumerate(node_ids_by_split)
        if split_node_ids.numel() == 0
    ]
    if len(empty_split_indices) > 0:
        final_ratios = self.get_split_ratios(node_ids_by_split)
        raise ValueError(
            f"Splitting produced splits {empty_split_indices} "
            f"with no nodes. Final ratios: {final_ratios}."
        )

get_split_ratios(node_ids_by_split)

Compute realized split ratios from node counts.

Parameters:

Name Type Description Default
node_ids_by_split list[Tensor]

Node IDs assigned to each split.

required

Returns:

Name Type Description
ratios list[float]

Ratios derived from the number of nodes in each split.

Source code in hypertorch/data/splitter.py
def get_split_ratios(self, node_ids_by_split: list[Tensor]) -> list[float]:
    """
    Compute realized split ratios from node counts.

    Args:
        node_ids_by_split: Node IDs assigned to each split.

    Returns:
        ratios: Ratios derived from the number of nodes in each split.
    """
    num_nodes_by_split = [int(split_node_ids.numel()) for split_node_ids in node_ids_by_split]
    num_nodes = sum(num_nodes_by_split)
    if num_nodes == 0:
        return [0.0 for _ in node_ids_by_split]

    return [round(split_num_nodes / num_nodes, 4) for split_num_nodes in num_nodes_by_split]

split(to_split, **kwargs)

Split node IDs by cumulative ratio boundaries.

Early splits use cumulative floor boundaries to avoid over-consuming nodes. The final split receives any remaining nodes caused by rounding.

Parameters:

Name Type Description Default
to_split Tensor

Node IDs to partition.

required
kwargs Any

ratios: Desired split ratios, used for initial split construction and as a reference during rebalancing. Expected as a keyword argument.

{}

Returns:

Name Type Description
node_ids_by_split list[Tensor]

The updated node IDs for each split.

ratios list[float]

The final split ratios.

Source code in hypertorch/data/splitter.py
def split(self, to_split: Tensor, **kwargs: Any) -> tuple[list[Tensor], list[float]]:
    """
    Split node IDs by cumulative ratio boundaries.

    Early splits use cumulative floor boundaries to avoid over-consuming nodes.
    The final split receives any remaining nodes caused by rounding.

    Args:
        to_split: Node IDs to partition.
        kwargs:
            ratios: Desired split ratios, used for initial split construction and
                as a reference during rebalancing. Expected as a keyword argument.

    Returns:
        node_ids_by_split: The updated node IDs for each split.
        ratios: The final split ratios.
    """
    ratios: list[float] = kwargs.get("ratios", [])
    validate_ratios(ratios)

    # Cumulative floor boundaries keep early splits from over-consuming nodes.
    # The last split absorbs any rounding remainder.
    num_nodes = int(to_split.size(0))

    start = 0
    cumulative_ratio = 0.0
    node_ids_by_split = []
    for split_idx, ratio in enumerate(ratios):
        cumulative_ratio += ratio
        end = num_nodes if split_idx == len(ratios) - 1 else int(cumulative_ratio * num_nodes)
        node_ids_by_split.append(to_split[start:end])
        start = end

    return node_ids_by_split, self.get_split_ratios(node_ids_by_split)

SparseHyperedgeDatasetSplitter

Bases: Splitter['Dataset', tuple[list['Dataset'], list[float]]]

Split a dataset by materializing sparse hyperedge partitions.

In the transductive setting, only the configured train split keeps the full node space, while the other splits are local sub-hypergraphs.

Attributes:

Name Type Description
node_space_setting NodeSpaceSetting

Whether to preserve full or local node spaces.

shuffle bool | None

Whether to shuffle hyperedges before splitting.

seed int | None

Random seed used when shuffling.

Source code in hypertorch/data/splitter.py
class SparseHyperedgeDatasetSplitter(Splitter["Dataset", tuple[list["Dataset"], list[float]]]):
    """
    Split a dataset by materializing sparse hyperedge partitions.

    In the transductive setting, only the configured train split keeps the full node space,
    while the other splits are local sub-hypergraphs.

    Attributes:
        node_space_setting: Whether to preserve full or local node spaces.
        shuffle: Whether to shuffle hyperedges before splitting.
        seed: Random seed used when shuffling.
    """

    def __init__(
        self,
        node_space_setting: NodeSpaceSetting = "transductive",
        shuffle: bool | None = False,
        seed: int | None = None,
    ) -> None:
        """
        Initialize the sparse dataset splitter.

        Args:
            node_space_setting: Whether to preserve full or local node spaces.
            shuffle: Whether to shuffle hyperedges before splitting. Defaults to ``False``.
            seed: Optional random seed for reproducibility. Defaults to ``None``.
        """
        self.node_space_setting: NodeSpaceSetting = node_space_setting
        self.shuffle: bool | None = shuffle
        self.seed: int | None = seed

        validate_node_space_setting(self.node_space_setting)

    def split(self, to_split: Dataset, **kwargs: Any) -> tuple[list[Dataset], list[float]]:
        """
        Split a dataset and return materialized sparse split datasets plus final ratios.

        Args:
            to_split: The dataset to split.
            kwargs:
                ratios: Desired split ratios. Expected as a keyword argument.
                cover_all_nodes_in_train_split: Whether transductive splits should move
                    hyperedges into the train split until all nodes are covered.
                train_split_idx: The index of the split to treat as the train split.

        Returns:
            split_datasets: The list of split datasets.
            final_ratios: The list of final hyperedge-count ratios.
        """
        if not to_split.hdata.is_hyperedge_related_task:
            raise ValueError(
                f"Cannot split dataset with task '{to_split.hdata.task}' because it is not "
                "a hyperedge-related task. Use 'NodeDatasetSplitter' for node-related tasks."
            )

        ratios: list[float] = kwargs.get("ratios", [])
        validate_ratios(ratios)

        cover_all_nodes_in_train_split: bool = kwargs.get("cover_all_nodes_in_train_split", False)

        train_split_idx: int = kwargs.get("train_split_idx", 0)
        self.__validate_train_split_idx(train_split_idx, ratios)

        hdata = to_split.hdata
        hyperedge_splitter = HyperedgeIDSplitter(
            hyperedge_index=hdata.hyperedge_index,
            num_nodes=hdata.num_nodes,
            num_hyperedges=hdata.num_hyperedges,
        )
        hyperedge_ids_permutation = hyperedge_splitter.get_hyperedge_ids_permutation(
            shuffle=self.shuffle,
            seed=self.seed,
        )
        hyperedge_ids_by_split, final_ratios = hyperedge_splitter.split(
            to_split=hyperedge_ids_permutation,
            ratios=ratios,
        )
        if is_transductive_setting(self.node_space_setting) and cover_all_nodes_in_train_split:
            hyperedge_ids_by_split, final_ratios = hyperedge_splitter.ensure_split_covers_all_nodes(
                hyperedge_ids_by_split=hyperedge_ids_by_split,
                split_idx=train_split_idx,
            )
        hyperedge_splitter.validate_splits_have_hyperedges(hyperedge_ids_by_split)

        split_datasets: list[Dataset] = []
        for split_num, split_hyperedge_ids in enumerate(hyperedge_ids_by_split):
            split_node_space_setting: NodeSpaceSetting = (
                "transductive"
                if is_transductive_setting(self.node_space_setting) and split_num == train_split_idx
                else "inductive"
            )
            split_hdata = SparseHyperedgeHDataSplitter(
                node_space_setting=split_node_space_setting
            ).split(
                to_split=hdata,
                split_hyperedge_ids=split_hyperedge_ids,
            )
            split_hdata = split_hdata.to(device=hdata.device)

            split_dataset = to_split.__class__(
                hdata=split_hdata,
                sampling_strategy=to_split.sampling_strategy,
            )
            split_datasets.append(split_dataset)

        return split_datasets, final_ratios

    def __validate_train_split_idx(self, train_split_idx: int, ratios: list[float]) -> None:
        """
        Validate the split index used as the transductive train split.

        Args:
            train_split_idx: Index of the split treated as the train split.
            ratios: Requested split ratios.

        Raises:
            ValueError: If the index is incompatible with the node-space setting or ratios.
        """
        if self.node_space_setting != "transductive" and train_split_idx != 0:
            raise ValueError(
                f"'train_split_idx' is only relevant when 'node_space_setting' is 'transductive', "
                f"got 'node_space_setting={self.node_space_setting}' and"
                f" 'train_split_idx={train_split_idx}'."
                "For the 'inductive' setting, splits are returned based on the provided ratios."
            )
        validate_is_between("train_split_idx", train_split_idx, 0, len(ratios) - 1)

__init__(node_space_setting='transductive', shuffle=False, seed=None)

Initialize the sparse dataset splitter.

Parameters:

Name Type Description Default
node_space_setting NodeSpaceSetting

Whether to preserve full or local node spaces.

'transductive'
shuffle bool | None

Whether to shuffle hyperedges before splitting. Defaults to False.

False
seed int | None

Optional random seed for reproducibility. Defaults to None.

None
Source code in hypertorch/data/splitter.py
def __init__(
    self,
    node_space_setting: NodeSpaceSetting = "transductive",
    shuffle: bool | None = False,
    seed: int | None = None,
) -> None:
    """
    Initialize the sparse dataset splitter.

    Args:
        node_space_setting: Whether to preserve full or local node spaces.
        shuffle: Whether to shuffle hyperedges before splitting. Defaults to ``False``.
        seed: Optional random seed for reproducibility. Defaults to ``None``.
    """
    self.node_space_setting: NodeSpaceSetting = node_space_setting
    self.shuffle: bool | None = shuffle
    self.seed: int | None = seed

    validate_node_space_setting(self.node_space_setting)

split(to_split, **kwargs)

Split a dataset and return materialized sparse split datasets plus final ratios.

Parameters:

Name Type Description Default
to_split Dataset

The dataset to split.

required
kwargs Any

ratios: Desired split ratios. Expected as a keyword argument. cover_all_nodes_in_train_split: Whether transductive splits should move hyperedges into the train split until all nodes are covered. train_split_idx: The index of the split to treat as the train split.

{}

Returns:

Name Type Description
split_datasets list[Dataset]

The list of split datasets.

final_ratios list[float]

The list of final hyperedge-count ratios.

Source code in hypertorch/data/splitter.py
def split(self, to_split: Dataset, **kwargs: Any) -> tuple[list[Dataset], list[float]]:
    """
    Split a dataset and return materialized sparse split datasets plus final ratios.

    Args:
        to_split: The dataset to split.
        kwargs:
            ratios: Desired split ratios. Expected as a keyword argument.
            cover_all_nodes_in_train_split: Whether transductive splits should move
                hyperedges into the train split until all nodes are covered.
            train_split_idx: The index of the split to treat as the train split.

    Returns:
        split_datasets: The list of split datasets.
        final_ratios: The list of final hyperedge-count ratios.
    """
    if not to_split.hdata.is_hyperedge_related_task:
        raise ValueError(
            f"Cannot split dataset with task '{to_split.hdata.task}' because it is not "
            "a hyperedge-related task. Use 'NodeDatasetSplitter' for node-related tasks."
        )

    ratios: list[float] = kwargs.get("ratios", [])
    validate_ratios(ratios)

    cover_all_nodes_in_train_split: bool = kwargs.get("cover_all_nodes_in_train_split", False)

    train_split_idx: int = kwargs.get("train_split_idx", 0)
    self.__validate_train_split_idx(train_split_idx, ratios)

    hdata = to_split.hdata
    hyperedge_splitter = HyperedgeIDSplitter(
        hyperedge_index=hdata.hyperedge_index,
        num_nodes=hdata.num_nodes,
        num_hyperedges=hdata.num_hyperedges,
    )
    hyperedge_ids_permutation = hyperedge_splitter.get_hyperedge_ids_permutation(
        shuffle=self.shuffle,
        seed=self.seed,
    )
    hyperedge_ids_by_split, final_ratios = hyperedge_splitter.split(
        to_split=hyperedge_ids_permutation,
        ratios=ratios,
    )
    if is_transductive_setting(self.node_space_setting) and cover_all_nodes_in_train_split:
        hyperedge_ids_by_split, final_ratios = hyperedge_splitter.ensure_split_covers_all_nodes(
            hyperedge_ids_by_split=hyperedge_ids_by_split,
            split_idx=train_split_idx,
        )
    hyperedge_splitter.validate_splits_have_hyperedges(hyperedge_ids_by_split)

    split_datasets: list[Dataset] = []
    for split_num, split_hyperedge_ids in enumerate(hyperedge_ids_by_split):
        split_node_space_setting: NodeSpaceSetting = (
            "transductive"
            if is_transductive_setting(self.node_space_setting) and split_num == train_split_idx
            else "inductive"
        )
        split_hdata = SparseHyperedgeHDataSplitter(
            node_space_setting=split_node_space_setting
        ).split(
            to_split=hdata,
            split_hyperedge_ids=split_hyperedge_ids,
        )
        split_hdata = split_hdata.to(device=hdata.device)

        split_dataset = to_split.__class__(
            hdata=split_hdata,
            sampling_strategy=to_split.sampling_strategy,
        )
        split_datasets.append(split_dataset)

    return split_datasets, final_ratios

__validate_train_split_idx(train_split_idx, ratios)

Validate the split index used as the transductive train split.

Parameters:

Name Type Description Default
train_split_idx int

Index of the split treated as the train split.

required
ratios list[float]

Requested split ratios.

required

Raises:

Type Description
ValueError

If the index is incompatible with the node-space setting or ratios.

Source code in hypertorch/data/splitter.py
def __validate_train_split_idx(self, train_split_idx: int, ratios: list[float]) -> None:
    """
    Validate the split index used as the transductive train split.

    Args:
        train_split_idx: Index of the split treated as the train split.
        ratios: Requested split ratios.

    Raises:
        ValueError: If the index is incompatible with the node-space setting or ratios.
    """
    if self.node_space_setting != "transductive" and train_split_idx != 0:
        raise ValueError(
            f"'train_split_idx' is only relevant when 'node_space_setting' is 'transductive', "
            f"got 'node_space_setting={self.node_space_setting}' and"
            f" 'train_split_idx={train_split_idx}'."
            "For the 'inductive' setting, splits are returned based on the provided ratios."
        )
    validate_is_between("train_split_idx", train_split_idx, 0, len(ratios) - 1)

SparseHyperedgeHDataSplitter

Bases: Splitter['HData', 'HData']

Materialize sparse HData splits from explicit hyperedge IDs.

Attributes:

Name Type Description
node_space_setting NodeSpaceSetting

Whether to preserve the full node space in the split.

Source code in hypertorch/data/splitter.py
class SparseHyperedgeHDataSplitter(Splitter["HData", "HData"]):
    """
    Materialize sparse HData splits from explicit hyperedge IDs.

    Attributes:
        node_space_setting: Whether to preserve the full node space in the split.
    """

    def __init__(self, node_space_setting: NodeSpaceSetting = "transductive") -> None:
        """
        Initialize the sparse HData splitter.

        Args:
            node_space_setting: Whether to preserve the full node space in the split.
        """
        self.node_space_setting: NodeSpaceSetting = node_space_setting
        validate_node_space_setting(self.node_space_setting)

    def split(self, to_split: HData, **kwargs: Any) -> HData:
        """
        Build an HData for a single split from the given hyperedge IDs.

        Args:
            to_split: The original HData containing the full hypergraph.
            kwargs:
                split_hyperedge_ids: The hyperedge IDs that should be included in the split,
                    expected as a keyword argument.

        Returns:
            hdata: The splitted instance with remapped node and hyperedge IDs.
        """
        if not to_split.is_hyperedge_related_task:
            raise ValueError(
                f"Cannot split dataset with task '{to_split.task}' because it is not "
                "a hyperedge-related task. Use 'NodeHDataSplitter' for node-related tasks."
            )

        split_hyperedge_ids_list = kwargs.get("split_hyperedge_ids", [])
        validate_is_non_empty("split_hyperedge_ids", split_hyperedge_ids_list)
        split_hyperedge_ids = torch.as_tensor(
            split_hyperedge_ids_list,
            dtype=torch.long,
            device=to_split.device,
        )

        keep_mask = torch.isin(to_split.hyperedge_index[1], split_hyperedge_ids)
        split_hyperedge_index = to_split.hyperedge_index[:, keep_mask]
        split_unique_hyperedge_ids = split_hyperedge_index[1].unique()

        split_y = to_split.y[split_unique_hyperedge_ids]

        split_hyperedge_attr = (
            to_split.hyperedge_attr[split_unique_hyperedge_ids]
            if to_split.hyperedge_attr is not None
            else None
        )
        split_hyperedge_weights = (
            to_split.hyperedge_weights[split_unique_hyperedge_ids]
            if to_split.hyperedge_weights is not None
            else None
        )
        split_target_hyperedge_mask = to_split.target_hyperedge_mask[split_unique_hyperedge_ids]

        if is_transductive_setting(self.node_space_setting):
            split_hyperedge_index[1] = to_0based_ids(
                original_ids=split_hyperedge_index[1],
                ids_to_rebase=split_unique_hyperedge_ids,
            )
            return to_split.__class__(
                x=to_split.x.clone(),
                hyperedge_index=split_hyperedge_index,
                hyperedge_weights=split_hyperedge_weights,
                hyperedge_attr=split_hyperedge_attr,
                num_nodes=to_split.num_nodes,
                num_hyperedges=len(split_unique_hyperedge_ids),
                global_node_ids=to_split.global_node_ids.clone(),
                target_node_mask=to_split.target_node_mask.clone(),
                target_hyperedge_mask=split_target_hyperedge_mask,
                y=split_y,
                task=to_split.task,
            )

        split_unique_node_ids = split_hyperedge_index[0].unique()
        split_hyperedge_index = (
            HyperedgeIndex(split_hyperedge_index)
            .to_0based(
                node_ids_to_rebase=split_unique_node_ids,
                hyperedge_ids_to_rebase=split_unique_hyperedge_ids,
            )
            .item
        )

        return to_split.__class__(
            x=to_split.x[split_unique_node_ids],
            hyperedge_index=split_hyperedge_index.clone(),
            hyperedge_weights=split_hyperedge_weights,
            hyperedge_attr=split_hyperedge_attr,
            num_nodes=len(split_unique_node_ids),
            num_hyperedges=len(split_unique_hyperedge_ids),
            global_node_ids=to_split.global_node_ids[split_unique_node_ids],
            target_node_mask=to_split.target_node_mask[split_unique_node_ids],
            target_hyperedge_mask=split_target_hyperedge_mask,
            y=split_y,
            task=to_split.task,
        )

__init__(node_space_setting='transductive')

Initialize the sparse HData splitter.

Parameters:

Name Type Description Default
node_space_setting NodeSpaceSetting

Whether to preserve the full node space in the split.

'transductive'
Source code in hypertorch/data/splitter.py
def __init__(self, node_space_setting: NodeSpaceSetting = "transductive") -> None:
    """
    Initialize the sparse HData splitter.

    Args:
        node_space_setting: Whether to preserve the full node space in the split.
    """
    self.node_space_setting: NodeSpaceSetting = node_space_setting
    validate_node_space_setting(self.node_space_setting)

split(to_split, **kwargs)

Build an HData for a single split from the given hyperedge IDs.

Parameters:

Name Type Description Default
to_split HData

The original HData containing the full hypergraph.

required
kwargs Any

split_hyperedge_ids: The hyperedge IDs that should be included in the split, expected as a keyword argument.

{}

Returns:

Name Type Description
hdata HData

The splitted instance with remapped node and hyperedge IDs.

Source code in hypertorch/data/splitter.py
def split(self, to_split: HData, **kwargs: Any) -> HData:
    """
    Build an HData for a single split from the given hyperedge IDs.

    Args:
        to_split: The original HData containing the full hypergraph.
        kwargs:
            split_hyperedge_ids: The hyperedge IDs that should be included in the split,
                expected as a keyword argument.

    Returns:
        hdata: The splitted instance with remapped node and hyperedge IDs.
    """
    if not to_split.is_hyperedge_related_task:
        raise ValueError(
            f"Cannot split dataset with task '{to_split.task}' because it is not "
            "a hyperedge-related task. Use 'NodeHDataSplitter' for node-related tasks."
        )

    split_hyperedge_ids_list = kwargs.get("split_hyperedge_ids", [])
    validate_is_non_empty("split_hyperedge_ids", split_hyperedge_ids_list)
    split_hyperedge_ids = torch.as_tensor(
        split_hyperedge_ids_list,
        dtype=torch.long,
        device=to_split.device,
    )

    keep_mask = torch.isin(to_split.hyperedge_index[1], split_hyperedge_ids)
    split_hyperedge_index = to_split.hyperedge_index[:, keep_mask]
    split_unique_hyperedge_ids = split_hyperedge_index[1].unique()

    split_y = to_split.y[split_unique_hyperedge_ids]

    split_hyperedge_attr = (
        to_split.hyperedge_attr[split_unique_hyperedge_ids]
        if to_split.hyperedge_attr is not None
        else None
    )
    split_hyperedge_weights = (
        to_split.hyperedge_weights[split_unique_hyperedge_ids]
        if to_split.hyperedge_weights is not None
        else None
    )
    split_target_hyperedge_mask = to_split.target_hyperedge_mask[split_unique_hyperedge_ids]

    if is_transductive_setting(self.node_space_setting):
        split_hyperedge_index[1] = to_0based_ids(
            original_ids=split_hyperedge_index[1],
            ids_to_rebase=split_unique_hyperedge_ids,
        )
        return to_split.__class__(
            x=to_split.x.clone(),
            hyperedge_index=split_hyperedge_index,
            hyperedge_weights=split_hyperedge_weights,
            hyperedge_attr=split_hyperedge_attr,
            num_nodes=to_split.num_nodes,
            num_hyperedges=len(split_unique_hyperedge_ids),
            global_node_ids=to_split.global_node_ids.clone(),
            target_node_mask=to_split.target_node_mask.clone(),
            target_hyperedge_mask=split_target_hyperedge_mask,
            y=split_y,
            task=to_split.task,
        )

    split_unique_node_ids = split_hyperedge_index[0].unique()
    split_hyperedge_index = (
        HyperedgeIndex(split_hyperedge_index)
        .to_0based(
            node_ids_to_rebase=split_unique_node_ids,
            hyperedge_ids_to_rebase=split_unique_hyperedge_ids,
        )
        .item
    )

    return to_split.__class__(
        x=to_split.x[split_unique_node_ids],
        hyperedge_index=split_hyperedge_index.clone(),
        hyperedge_weights=split_hyperedge_weights,
        hyperedge_attr=split_hyperedge_attr,
        num_nodes=len(split_unique_node_ids),
        num_hyperedges=len(split_unique_hyperedge_ids),
        global_node_ids=to_split.global_node_ids[split_unique_node_ids],
        target_node_mask=to_split.target_node_mask[split_unique_node_ids],
        target_hyperedge_mask=split_target_hyperedge_mask,
        y=split_y,
        task=to_split.task,
    )

Splitter

Bases: ABC, Generic[_ToSplitType, _SplitResultType]

Abstract base class for splitting objects into parts.

Source code in hypertorch/data/splitter.py
class Splitter(ABC, Generic[_ToSplitType, _SplitResultType]):
    """
    Abstract base class for splitting objects into parts.
    """

    @abstractmethod
    def split(self, to_split: _ToSplitType, **kwargs: Any) -> _SplitResultType:
        """
        Split the input object and return the split result.

        Args:
            to_split: The object to split.
            **kwargs: Additional keyword arguments that may be required by specific splitter
                implementations.

        Returns:
            The result of splitting the input object.
        """
        pass

split(to_split, **kwargs) abstractmethod

Split the input object and return the split result.

Parameters:

Name Type Description Default
to_split _ToSplitType

The object to split.

required
**kwargs Any

Additional keyword arguments that may be required by specific splitter implementations.

{}

Returns:

Type Description
_SplitResultType

The result of splitting the input object.

Source code in hypertorch/data/splitter.py
@abstractmethod
def split(self, to_split: _ToSplitType, **kwargs: Any) -> _SplitResultType:
    """
    Split the input object and return the split result.

    Args:
        to_split: The object to split.
        **kwargs: Additional keyword arguments that may be required by specific splitter
            implementations.

    Returns:
        The result of splitting the input object.
    """
    pass

list_datasets()

Return supported preloaded dataset names in deterministic order.

Source code in hypertorch/data/supported_datasets.py
def list_datasets() -> list[str]:
    """
    Return supported preloaded dataset names in deterministic order.
    """
    return sorted(_PreloadedDataset._registry)

get_dataset_by_name(dataset_name)

Instantiate a supported dataset by name.

Parameters:

Name Type Description Default
dataset_name str

Name returned by list_datasets.

required

Returns:

Name Type Description
dataset Dataset

Loaded dataset instance.

Raises:

Type Description
ValueError

If the dataset name is not registered.

Source code in hypertorch/data/supported_datasets.py
def get_dataset_by_name(dataset_name: str) -> Dataset:
    """
    Instantiate a supported dataset by name.

    Args:
        dataset_name: Name returned by ``list_datasets``.

    Returns:
        dataset: Loaded dataset instance.

    Raises:
        ValueError: If the dataset name is not registered.
    """
    dataset_cls = _PreloadedDataset._registry.get(dataset_name)
    if dataset_cls is None:
        raise ValueError(f"Dataset not found: {dataset_name}")
    return dataset_cls()

create_sampler_from_strategy(strategy)

Factory function to create a sampler instance based on the provided sampling strategy type.

Parameters:

Name Type Description Default
strategy SamplingStrategy

An instance of SamplingStrategy enum indicating which sampling strategy to use.

required

Returns:

Name Type Description
sampler BaseSampler

An instance of a subclass of BaseSampler corresponding to the specified strategy.

Raises:

Type Description
ValueError

If strategy is not a supported SamplingStrategy.

Source code in hypertorch/data/sampler.py
def create_sampler_from_strategy(strategy: SamplingStrategy) -> BaseSampler:
    """
    Factory function to create a sampler instance based on the provided sampling strategy type.

    Args:
        strategy: An instance of SamplingStrategy enum indicating which sampling strategy to use.

    Returns:
        sampler: An instance of a subclass of BaseSampler corresponding to the specified strategy.

    Raises:
        ValueError: If ``strategy`` is not a supported `SamplingStrategy`.
    """
    match strategy:
        case SamplingStrategyEnum.NODE:
            return NodeSampler()
        case SamplingStrategyEnum.HYPEREDGE:
            return HyperedgeSampler()
        case _:
            raise ValueError(f"Unsupported sampling strategy: {strategy!r}.")