• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ContinualAI / avalanche / 5399886876

pending completion
5399886876

Pull #1398

github

web-flow
Merge 2c8aba8e6 into a61ae5cab
Pull Request #1398: switch to black formatting

1023 of 1372 new or added lines in 177 files covered. (74.56%)

144 existing lines in 66 files now uncovered.

16366 of 22540 relevant lines covered (72.61%)

2.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.33
/avalanche/benchmarks/scenarios/new_classes/nc_scenario.py
1
################################################################################
2
# Copyright (c) 2021 ContinualAI.                                              #
3
# Copyrights licensed under the MIT License.                                   #
4
# See the accompanying LICENSE file for terms.                                 #
5
#                                                                              #
6
# Date: 12-05-2020                                                             #
7
# Author(s): Lorenzo Pellegrini                                                #
8
# E-mail: contact@continualai.org                                              #
9
# Website: avalanche.continualai.org                                           #
10
################################################################################
11

12
from typing import Sequence, List, Optional, Dict, Any, Set
4✔
13

14
import torch
4✔
15

16
from avalanche.benchmarks.scenarios.classification_scenario import (
4✔
17
    ClassificationScenario,
18
    ClassificationStream,
19
    ClassificationExperience,
20
)
21
from avalanche.benchmarks.utils import classification_subset
4✔
22
from avalanche.benchmarks.utils.classification_dataset import (
4✔
23
    ClassificationDataset,
24
    SupervisedClassificationDataset,
25
)
26

27
from avalanche.benchmarks.utils.flat_data import ConstantSequence
4✔
28

29

30
class NCScenario(
4✔
31
    ClassificationScenario["NCStream", "NCExperience", SupervisedClassificationDataset]
32
):
33

34
    """
4✔
35
    This class defines a "New Classes" scenario. Once created, an instance
36
    of this class can be iterated in order to obtain the experience sequence
37
    under the form of instances of :class:`NCExperience`.
38

39
    This class can be used directly. However, we recommend using facilities like
40
    :func:`avalanche.benchmarks.generators.nc_benchmark`.
41
    """
42

43
    def __init__(
4✔
44
        self,
45
        train_dataset: ClassificationDataset,
46
        test_dataset: ClassificationDataset,
47
        n_experiences: int,
48
        task_labels: bool,
49
        shuffle: bool = True,
50
        seed: Optional[int] = None,
51
        fixed_class_order: Optional[Sequence[int]] = None,
52
        per_experience_classes: Optional[Dict[int, int]] = None,
53
        class_ids_from_zero_from_first_exp: bool = False,
54
        class_ids_from_zero_in_each_exp: bool = False,
55
        reproducibility_data: Optional[Dict[str, Any]] = None,
56
    ):
57
        """
58
        Creates a ``NCGenericScenario`` instance given the training and test
59
        Datasets and the number of experiences.
60

61
        By default, the number of classes will be automatically detected by
62
        looking at the training Dataset ``targets`` field. Classes will be
63
        uniformly distributed across ``n_experiences`` unless a
64
        ``per_experience_classes`` argument is specified.
65

66
        The number of classes must be divisible without remainder by the number
67
        of experiences. This also applies when the ``per_experience_classes``
68
        argument is not None.
69

70
        :param train_dataset: The training dataset. The dataset must be a
71
            subclass of :class:`AvalancheDataset`. For instance, one can
72
            use the datasets from the torchvision package like that:
73
            ``train_dataset=AvalancheDataset(torchvision_dataset)``.
74
        :param test_dataset: The test dataset. The dataset must be a
75
            subclass of :class:`AvalancheDataset`. For instance, one can
76
            use the datasets from the torchvision package like that:
77
            ``test_dataset=AvalancheDataset(torchvision_dataset)``.
78
        :param n_experiences: The number of experiences.
79
        :param task_labels: If True, each experience will have an ascending task
80
            label. If False, the task label will be 0 for all the experiences.
81
        :param shuffle: If True, the class order will be shuffled. Defaults to
82
            True.
83
        :param seed: If shuffle is True and seed is not None, the class order
84
            will be shuffled according to the seed. When None, the current
85
            PyTorch random number generator state will be used.
86
            Defaults to None.
87
        :param fixed_class_order: If not None, the class order to use (overrides
88
            the shuffle argument). Very useful for enhancing
89
            reproducibility. Defaults to None.
90
        :param per_experience_classes: Is not None, a dictionary whose keys are
91
            (0-indexed) experience IDs and their values are the number of
92
            classes to include in the respective experiences. The dictionary
93
            doesn't have to contain a key for each experience! All the remaining
94
            experiences will contain an equal amount of the remaining classes.
95
            The remaining number of classes must be divisible without remainder
96
            by the remaining number of experiences. For instance,
97
            if you want to include 50 classes in the first experience
98
            while equally distributing remaining classes across remaining
99
            experiences, just pass the "{0: 50}" dictionary as the
100
            per_experience_classes parameter. Defaults to None.
101
        :param class_ids_from_zero_from_first_exp: If True, original class IDs
102
            will be remapped so that they will appear as having an ascending
103
            order. For instance, if the resulting class order after shuffling
104
            (or defined by fixed_class_order) is [23, 34, 11, 7, 6, ...] and
105
            class_ids_from_zero_from_first_exp is True, then all the patterns
106
            belonging to class 23 will appear as belonging to class "0",
107
            class "34" will be mapped to "1", class "11" to "2" and so on.
108
            This is very useful when drawing confusion matrices and when dealing
109
            with algorithms with dynamic head expansion. Defaults to False.
110
            Mutually exclusive with the ``class_ids_from_zero_in_each_exp``
111
            parameter.
112
        :param class_ids_from_zero_in_each_exp: If True, original class IDs
113
            will be mapped to range [0, n_classes_in_exp) for each experience.
114
            Defaults to False. Mutually exclusive with the
115
            ``class_ids_from_zero_from_first_exp parameter``.
116
        :param reproducibility_data: If not None, overrides all the other
117
            scenario definition options. This is usually a dictionary containing
118
            data used to reproduce a specific experiment. One can use the
119
            ``get_reproducibility_data`` method to get (and even distribute)
120
            the experiment setup so that it can be loaded by passing it as this
121
            parameter. In this way one can be sure that the same specific
122
            experimental setup is being used (for reproducibility purposes).
123
            Beware that, in order to reproduce an experiment, the same train and
124
            test datasets must be used. Defaults to None.
125
        """
126

127
        if not isinstance(train_dataset, SupervisedClassificationDataset):
4✔
128
            train_dataset = SupervisedClassificationDataset(train_dataset)
×
129
        if not isinstance(test_dataset, SupervisedClassificationDataset):
4✔
130
            test_dataset = SupervisedClassificationDataset(test_dataset)
×
131

132
        if class_ids_from_zero_from_first_exp and class_ids_from_zero_in_each_exp:
4✔
UNCOV
133
            raise ValueError(
×
134
                "Invalid mutually exclusive options "
135
                "class_ids_from_zero_from_first_exp and "
136
                "class_ids_from_zero_in_each_exp set at the "
137
                "same time"
138
            )
139
        if reproducibility_data:
4✔
140
            n_experiences = reproducibility_data["n_experiences"]
4✔
141

142
        if n_experiences < 1:
4✔
143
            raise ValueError(
×
144
                "Invalid number of experiences (n_experiences "
145
                "parameter): must be greater than 0"
146
            )
147

148
        self.classes_order: List[int] = []
4✔
149
        """ Stores the class order (remapped class IDs). """
1✔
150

151
        self.classes_order_original_ids: List[int] = torch.unique(
4✔
152
            torch.as_tensor(train_dataset.targets), sorted=True
153
        ).tolist()
154
        """ Stores the class order (original class IDs) """
1✔
155

156
        n_original_classes = max(self.classes_order_original_ids) + 1
4✔
157

158
        self.class_mapping: List[int] = []
4✔
159
        """
1✔
160
        class_mapping stores the class mapping so that 
161
        `mapped_class_id = class_mapping[original_class_id]`. 
162
        
163
        If the benchmark is created with an amount of classes which is less than
164
        the amount of all classes in the dataset, then class_mapping will 
165
        contain some -1 values corresponding to ignored classes. This can
166
        happen when passing a fixed class order to the constructor.
167
        """
168

169
        self.n_classes_per_exp: List[int] = []
4✔
170
        """ A list that, for each experience (identified by its index/ID),
1✔
171
            stores the number of classes assigned to that experience. """
172

173
        self._classes_in_exp: List[Set[int]] = []
4✔
174

175
        self.original_classes_in_exp: List[Set[int]] = []
4✔
176
        """
1✔
177
        A list that, for each experience (identified by its index/ID), stores a 
178
        set of the original IDs of classes assigned to that experience. 
179
        This field applies to both train and test streams.
180
        """
181

182
        self.class_ids_from_zero_from_first_exp: bool = (
4✔
183
            class_ids_from_zero_from_first_exp
184
        )
185
        """ If True the class IDs have been remapped to start from zero. """
1✔
186

187
        self.class_ids_from_zero_in_each_exp: bool = class_ids_from_zero_in_each_exp
4✔
188
        """ If True the class IDs have been remapped to start from zero in 
1✔
189
        each experience """
190

191
        # Note: if fixed_class_order is None and shuffle is False,
192
        # the class order will be the one encountered
193
        # By looking at the train_dataset targets field
194
        if reproducibility_data:
4✔
195
            self.classes_order_original_ids = reproducibility_data[
4✔
196
                "classes_order_original_ids"
197
            ]
198
            self.class_ids_from_zero_from_first_exp = reproducibility_data[
4✔
199
                "class_ids_from_zero_from_first_exp"
200
            ]
201
            self.class_ids_from_zero_in_each_exp = reproducibility_data[
4✔
202
                "class_ids_from_zero_in_each_exp"
203
            ]
204
        elif fixed_class_order is not None:
4✔
205
            # User defined class order -> just use it
206
            if len(
4✔
207
                set(self.classes_order_original_ids).union(set(fixed_class_order))
208
            ) != len(self.classes_order_original_ids):
209
                raise ValueError("Invalid classes defined in fixed_class_order")
×
210

211
            self.classes_order_original_ids = list(fixed_class_order)
4✔
212
        elif shuffle:
4✔
213
            # No user defined class order.
214
            # If a seed is defined, set the random number generator seed.
215
            # If no seed has been defined, use the actual
216
            # random number generator state.
217
            # Finally, shuffle the class list to obtain a random classes
218
            # order
219
            if seed is not None:
4✔
220
                torch.random.manual_seed(seed)
4✔
221
            self.classes_order_original_ids = torch.as_tensor(
4✔
222
                self.classes_order_original_ids
223
            )[torch.randperm(len(self.classes_order_original_ids))].tolist()
224

225
        self.n_classes: int = len(self.classes_order_original_ids)
4✔
226
        """ The number of classes """
1✔
227

228
        if reproducibility_data:
4✔
229
            self.n_classes_per_exp = reproducibility_data["n_classes_per_exp"]
4✔
230
        elif per_experience_classes is not None:
4✔
231
            # per_experience_classes is a user-defined dictionary that defines
232
            # the number of classes to include in some (or all) experiences.
233
            # Remaining classes are equally distributed across the other
234
            # experiences.
235
            #
236
            # Format of per_experience_classes dictionary:
237
            #   - key = experience id
238
            #   - value = number of classes for this experience
239

240
            if (
4✔
241
                max(per_experience_classes.keys()) >= n_experiences
242
                or min(per_experience_classes.keys()) < 0
243
            ):
244
                # The dictionary contains a key (that is, a experience id) >=
245
                # the number of requested experiences... or < 0
246
                raise ValueError(
×
247
                    "Invalid experience id in per_experience_classes parameter:"
248
                    " experience ids must be in range [0, n_experiences)"
249
                )
250
            if min(per_experience_classes.values()) < 0:
4✔
251
                # One or more values (number of classes for each experience) < 0
252
                raise ValueError(
×
253
                    "Wrong number of classes defined for one or "
254
                    "more experiences: must be a non-negative "
255
                    "value"
256
                )
257

258
            if sum(per_experience_classes.values()) > self.n_classes:
4✔
259
                # The sum of dictionary values (n. of classes for each
260
                # experience) >= the number of classes
261
                raise ValueError(
×
262
                    "Insufficient number of classes: "
263
                    "per_experience_classes parameter can't "
264
                    "be satisfied"
265
                )
266

267
            # Remaining classes are equally distributed across remaining
268
            # experiences. This amount of classes must be be divisible without
269
            # remainder by the number of remaining experiences
270
            remaining_exps = n_experiences - len(per_experience_classes)
4✔
271
            if (
4✔
272
                remaining_exps > 0
273
                and (self.n_classes - sum(per_experience_classes.values()))
274
                % remaining_exps
275
                > 0
276
            ):
277
                raise ValueError(
×
278
                    "Invalid number of experiences: remaining "
279
                    "classes cannot be divided by n_experiences"
280
                )
281

282
            # default_per_exp_classes is the default amount of classes
283
            # for the remaining experiences
284
            if remaining_exps > 0:
4✔
285
                default_per_exp_classes = (
4✔
286
                    self.n_classes - sum(per_experience_classes.values())
287
                ) // remaining_exps
288
            else:
289
                default_per_exp_classes = 0
4✔
290

291
            # Initialize the self.n_classes_per_exp list using
292
            # "default_per_exp_classes" as the default
293
            # amount of classes per experience. Then, loop through the
294
            # per_experience_classes dictionary to set the customized,
295
            # user defined, classes for the required experiences.
296
            self.n_classes_per_exp = [default_per_exp_classes] * n_experiences
4✔
297
            for exp_id in per_experience_classes:
4✔
298
                self.n_classes_per_exp[exp_id] = per_experience_classes[exp_id]
4✔
299
        else:
300
            # Classes will be equally distributed across the experiences
301
            # The amount of classes must be be divisible without remainder
302
            # by the number of experiences
303
            if self.n_classes % n_experiences > 0:
4✔
304
                raise ValueError(
×
305
                    f"Invalid number of experiences: classes contained in "
306
                    f"dataset ({self.n_classes}) cannot be divided by "
307
                    f"n_experiences ({n_experiences})"
308
                )
309
            self.n_classes_per_exp = [self.n_classes // n_experiences] * n_experiences
4✔
310

311
        # Before populating the classes_in_experience list,
312
        # define the remapped class IDs.
313
        if reproducibility_data:
4✔
314
            # Method 0: use reproducibility data
315
            self.classes_order = reproducibility_data["classes_order"]
4✔
316
            self.class_mapping = reproducibility_data["class_mapping"]
4✔
317
        elif self.class_ids_from_zero_from_first_exp:
4✔
318
            # Method 1: remap class IDs so that they appear in ascending order
319
            # over all experiences
320
            self.classes_order = list(range(0, self.n_classes))
4✔
321
            self.class_mapping = [-1] * n_original_classes
4✔
322
            for class_id in range(n_original_classes):
4✔
323
                # This check is needed because, when a fixed class order is
324
                # used, the user may have defined an amount of classes less than
325
                # the overall amount of classes in the dataset.
326
                if class_id in self.classes_order_original_ids:
4✔
327
                    self.class_mapping[
4✔
328
                        class_id
329
                    ] = self.classes_order_original_ids.index(class_id)
330
        elif self.class_ids_from_zero_in_each_exp:
4✔
331
            # Method 2: remap class IDs so that they appear in range [0, N] in
332
            # each experience
333
            self.classes_order = []
4✔
334
            self.class_mapping = [-1] * n_original_classes
4✔
335
            next_class_idx = 0
4✔
336
            for exp_id, exp_n_classes in enumerate(self.n_classes_per_exp):
4✔
337
                self.classes_order += list(range(exp_n_classes))
4✔
338
                for exp_class_idx in range(exp_n_classes):
4✔
339
                    original_class_position = next_class_idx + exp_class_idx
4✔
340
                    original_class_id = self.classes_order_original_ids[
4✔
341
                        original_class_position
342
                    ]
343
                    self.class_mapping[original_class_id] = exp_class_idx
4✔
344
                next_class_idx += exp_n_classes
4✔
345
        else:
346
            # Method 3: no remapping of any kind
347
            # remapped_id = class_mapping[class_id] -> class_id == remapped_id
348
            self.classes_order = self.classes_order_original_ids
4✔
349
            self.class_mapping = list(range(0, n_original_classes))
4✔
350

351
        original_training_dataset = train_dataset
4✔
352
        original_test_dataset = test_dataset
4✔
353

354
        # Populate the _classes_in_exp and original_classes_in_exp lists
355
        # "_classes_in_exp[exp_id]": list of (remapped) class IDs assigned
356
        # to experience "exp_id"
357
        # "original_classes_in_exp[exp_id]": list of original class IDs
358
        # assigned to experience "exp_id"
359
        for exp_id in range(n_experiences):
4✔
360
            classes_start_idx = sum(self.n_classes_per_exp[:exp_id])
4✔
361
            classes_end_idx = classes_start_idx + self.n_classes_per_exp[exp_id]
4✔
362

363
            self._classes_in_exp.append(
4✔
364
                set(self.classes_order[classes_start_idx:classes_end_idx])
365
            )
366
            self.original_classes_in_exp.append(
4✔
367
                set(self.classes_order_original_ids[classes_start_idx:classes_end_idx])
368
            )
369

370
        # Finally, create the experience -> patterns assignment.
371
        # In order to do this, we don't load all the patterns
372
        # instead we use the targets field.
373
        train_exps_patterns_assignment = []
4✔
374
        test_exps_patterns_assignment = []
4✔
375

376
        self._has_task_labels = task_labels
4✔
377
        if reproducibility_data is not None:
4✔
378
            self._has_task_labels = bool(reproducibility_data["has_task_labels"])
4✔
379

380
        pattern_train_task_labels: Sequence[int]
381
        pattern_test_task_labels: Sequence[int]
382
        if self._has_task_labels:
4✔
383
            pattern_train_task_labels = [-1] * len(train_dataset)
4✔
384
            pattern_test_task_labels = [-1] * len(test_dataset)
4✔
385
        else:
386
            pattern_train_task_labels = ConstantSequence(0, len(train_dataset))
4✔
387
            pattern_test_task_labels = ConstantSequence(0, len(test_dataset))
4✔
388

389
        for exp_id in range(n_experiences):
4✔
390
            selected_classes = self.original_classes_in_exp[exp_id]
4✔
391
            selected_indexes_train = []
4✔
392
            for sc in selected_classes:
4✔
393
                train_tgt_idx = original_training_dataset.targets.val_to_idx[sc]
4✔
394
                selected_indexes_train.extend(train_tgt_idx)
4✔
395
                if self._has_task_labels:
4✔
396
                    for idx in train_tgt_idx:
4✔
397
                        pattern_train_task_labels[idx] = exp_id
4✔
398

399
            selected_indexes_test = []
4✔
400
            for sc in selected_classes:
4✔
401
                test_tgt_idx = original_test_dataset.targets.val_to_idx[sc]
4✔
402
                selected_indexes_test.extend(test_tgt_idx)
4✔
403
                if self._has_task_labels:
4✔
404
                    for idx in test_tgt_idx:
4✔
405
                        pattern_test_task_labels[idx] = exp_id
4✔
406

407
            train_exps_patterns_assignment.append(selected_indexes_train)
4✔
408
            test_exps_patterns_assignment.append(selected_indexes_test)
4✔
409

410
        # Good idea, but doesn't work
411
        # transform_groups = train_eval_transforms(train_dataset, test_dataset)
412
        #
413
        # train_dataset = train_dataset\
414
        #     .replace_transforms(*transform_groups['train'], group='train') \
415
        #     .replace_transforms(*transform_groups['eval'], group='eval')
416
        #
417
        # test_dataset = test_dataset \
418
        #     .replace_transforms(*transform_groups['train'], group='train') \
419
        #     .replace_transforms(*transform_groups['eval'], group='eval')
420

421
        train_dataset = classification_subset(
4✔
422
            train_dataset,
423
            class_mapping=self.class_mapping,
424
            initial_transform_group="train",
425
        )
426
        test_dataset = classification_subset(
4✔
427
            test_dataset,
428
            class_mapping=self.class_mapping,
429
            initial_transform_group="eval",
430
        )
431

432
        self.train_exps_patterns_assignment = train_exps_patterns_assignment
4✔
433
        """ A list containing which training instances are assigned to each
1✔
434
        experience in the train stream. Instances are identified by their id 
435
        w.r.t. the dataset found in the original_train_dataset field. """
436

437
        self.test_exps_patterns_assignment = test_exps_patterns_assignment
4✔
438
        """ A list containing which test instances are assigned to each
1✔
439
        experience in the test stream. Instances are identified by their id 
440
        w.r.t. the dataset found in the original_test_dataset field. """
441

442
        train_experiences: List[ClassificationDataset] = []
4✔
443
        train_task_labels: List[int] = []
4✔
444
        for t_id, exp_def in enumerate(train_exps_patterns_assignment):
4✔
445
            if self._has_task_labels:
4✔
446
                train_task_labels.append(t_id)
4✔
447
            else:
448
                train_task_labels.append(0)
4✔
449
            exp_task_labels = ConstantSequence(
4✔
450
                train_task_labels[-1], len(train_dataset)
451
            )
452
            train_experiences.append(
4✔
453
                classification_subset(
454
                    train_dataset, indices=exp_def, task_labels=exp_task_labels
455
                )
456
            )
457

458
        test_experiences: List[ClassificationDataset] = []
4✔
459
        test_task_labels: List[int] = []
4✔
460
        for t_id, exp_def in enumerate(test_exps_patterns_assignment):
4✔
461
            if self._has_task_labels:
4✔
462
                test_task_labels.append(t_id)
4✔
463
            else:
464
                test_task_labels.append(0)
4✔
465

466
            exp_task_labels = ConstantSequence(test_task_labels[-1], len(test_dataset))
4✔
467
            test_experiences.append(
4✔
468
                classification_subset(
469
                    test_dataset, indices=exp_def, task_labels=exp_task_labels
470
                )
471
            )
472

473
        super().__init__(
4✔
474
            stream_definitions={
475
                "train": (train_experiences, train_task_labels, train_dataset),
476
                "test": (test_experiences, test_task_labels, test_dataset),
477
            },
478
            stream_factory=NCStream,
479
            experience_factory=NCExperience,
480
        )
481

482
    def get_reproducibility_data(self):
4✔
483
        reproducibility_data = {
4✔
484
            "class_ids_from_zero_from_first_exp": bool(
485
                self.class_ids_from_zero_from_first_exp
486
            ),
487
            "class_ids_from_zero_in_each_exp": bool(
488
                self.class_ids_from_zero_in_each_exp
489
            ),
490
            "class_mapping": self.class_mapping,
491
            "classes_order": self.classes_order,
492
            "classes_order_original_ids": self.classes_order_original_ids,
493
            "n_classes_per_exp": self.n_classes_per_exp,
494
            "n_experiences": int(self.n_experiences),
495
            "has_task_labels": self._has_task_labels,
496
        }
497
        return reproducibility_data
4✔
498

499
    def classes_in_exp_range(
4✔
500
        self, exp_start: int, exp_end: Optional[int] = None
501
    ) -> List[int]:
502
        """
503
        Gets a list of classes contained in the given experiences. The
504
        experiences are defined by range. This means that only the classes in
505
        range [exp_start, exp_end) will be included.
506

507
        :param exp_start: The starting experience ID.
508
        :param exp_end: The final experience ID. Can be None, which means that
509
            all the remaining experiences will be taken.
510

511
        :returns: The classes contained in the required experience range.
512
        """
513
        # Ref: https://stackoverflow.com/a/952952
514
        if exp_end is None:
4✔
515
            return [
4✔
516
                item
517
                for sublist in self.classes_in_experience["train"][exp_start:]
518
                for item in sublist
519
            ]
520

521
        return [
4✔
522
            item
523
            for sublist in self.classes_in_experience["train"][exp_start:exp_end]
524
            for item in sublist
525
        ]
526

527

528
class NCStream(ClassificationStream["NCExperience"]):
4✔
529
    def __init__(
4✔
530
        self,
531
        name: str,
532
        benchmark: NCScenario,
533
        *,
534
        slice_ids: Optional[List[int]] = None,
535
        set_stream_info: bool = True,
536
    ):
537
        self.benchmark: NCScenario = benchmark
4✔
538
        super().__init__(
4✔
539
            name=name,
540
            benchmark=benchmark,
541
            slice_ids=slice_ids,
542
            set_stream_info=set_stream_info,
543
        )
544

545

546
class NCExperience(ClassificationExperience[SupervisedClassificationDataset]):
4✔
547
    """
4✔
548
    Defines a "New Classes" experience. It defines fields to obtain the current
549
    dataset and the associated task label. It also keeps a reference to the
550
    stream from which this experience was taken.
551
    """
552

553
    def __init__(self, origin_stream: NCStream, current_experience: int):
4✔
554
        """
555
        Creates a ``NCExperience`` instance given the stream from this
556
        experience was taken and and the current experience ID.
557

558
        :param origin_stream: The stream from which this experience was
559
            obtained.
560
        :param current_experience: The current experience ID, as an integer.
561
        """
562

563
        self._benchmark: NCScenario = origin_stream.benchmark
4✔
564

565
        super().__init__(origin_stream, current_experience)
4✔
566

567
    @property  # type: ignore[override]
4✔
568
    def benchmark(self) -> NCScenario:
4✔
569
        bench = self._benchmark
4✔
570
        NCExperience._check_unset_attribute("benchmark", bench)
4✔
571
        return bench
4✔
572

573
    @benchmark.setter
4✔
574
    def benchmark(self, bench: NCScenario):
4✔
575
        self._benchmark = bench
×
576

577

578
__all__ = ["NCScenario", "NCStream", "NCExperience"]
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc