code-loader 1.0.195.dev1__tar.gz → 1.0.197.dev0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/PKG-INFO +1 -1
  2. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/contract/datasetclasses.py +53 -66
  3. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/inner_leap_binder/leapbinder.py +122 -29
  4. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/inner_leap_binder/leapbinder_decorators.py +906 -279
  5. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/leaploader.py +224 -265
  6. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/leaploaderbase.py +71 -3
  7. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/utils.py +51 -5
  8. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/pyproject.toml +2 -1
  9. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/LICENSE +0 -0
  10. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/README.md +0 -0
  11. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/__init__.py +0 -0
  12. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/contract/__init__.py +0 -0
  13. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/contract/enums.py +0 -0
  14. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/contract/exceptions.py +0 -0
  15. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/contract/mapping.py +0 -0
  16. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/contract/responsedataclasses.py +0 -0
  17. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/contract/sim_config.py +0 -0
  18. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/contract/visualizer_classes.py +0 -0
  19. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/default_losses.py +0 -0
  20. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/default_metrics.py +0 -0
  21. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/experiment_api/__init__.py +0 -0
  22. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/experiment_api/api.py +0 -0
  23. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/experiment_api/cli_config_utils.py +0 -0
  24. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/experiment_api/client.py +0 -0
  25. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/experiment_api/epoch.py +0 -0
  26. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/experiment_api/experiment.py +0 -0
  27. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/experiment_api/experiment_context.py +0 -0
  28. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/experiment_api/types.py +0 -0
  29. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/experiment_api/utils.py +0 -0
  30. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/experiment_api/workingspace_config_utils.py +0 -0
  31. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/inner_leap_binder/__init__.py +0 -0
  32. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/mixpanel_tracker.py +0 -0
  33. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/plot_functions/__init__.py +0 -0
  34. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/plot_functions/plot_functions.py +0 -0
  35. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/plot_functions/visualize.py +0 -0
  36. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/visualizers/__init__.py +0 -0
  37. {code_loader-1.0.195.dev1 → code_loader-1.0.197.dev0}/code_loader/visualizers/default_visualizers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: code-loader
3
- Version: 1.0.195.dev1
3
+ Version: 1.0.197.dev0
4
4
  Summary:
5
5
  Home-page: https://github.com/tensorleap/code-loader
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  import warnings
2
2
  from dataclasses import dataclass, field
3
- from typing import Any, Callable, List, Optional, Dict, Union, Type, Literal, cast
3
+ from typing import Any, Callable, List, Optional, Dict, Union, Type, Literal, Tuple
4
4
  import re
5
5
  import numpy as np
6
6
  import numpy.typing as npt
@@ -15,8 +15,6 @@ custom_latent_space_attribute = "custom_latent_space"
15
15
 
16
16
  _simulation_context: Dict[str, bool] = {"active": False}
17
17
 
18
- SampleId = Union[int, str]
19
-
20
18
 
21
19
  @dataclass
22
20
  class PreprocessResponse:
@@ -41,44 +39,27 @@ class PreprocessResponse:
41
39
  """
42
40
  length: Optional[int] = None # Deprecated. Please use sample_ids instead
43
41
  data: Any = None
44
- sample_ids: Optional[Union[List[SampleId], List[List[SampleId]]]] = None
42
+ sample_ids: Optional[Union[List[str], List[int]]] = None
45
43
  state: Optional[DataStateType] = None
46
44
  sample_id_type: Optional[Union[Type[str], Type[int]]] = None
47
45
  sample_ids_to_instance_mappings: Optional[Dict[str, List[str]]] = None # in use only for element instance
48
46
  instance_to_sample_ids_mappings: Optional[Dict[str, str]] = None # in use only for element instance
49
47
  tl_generated: bool = False
50
- _grouped: bool = field(default=False, init=False, repr=False, compare=False)
51
48
 
52
49
  def __post_init__(self) -> None:
53
50
  assert self.sample_ids_to_instance_mappings is None, f"Keep sample_ids_to_instance_mappings None when initializing PreprocessResponse"
54
51
  assert self.instance_to_sample_ids_mappings is None, f"Keep instance_to_sample_ids_mappings None when initializing PreprocessResponse"
55
52
 
56
53
  if self.length is not None and self.sample_ids is None:
57
- self.sample_ids = cast(List[SampleId], [i for i in range(self.length)])
54
+ self.sample_ids = [i for i in range(self.length)]
58
55
  self.sample_id_type = int
59
- self._grouped = False
60
56
  elif self.length is None and self.sample_ids is not None:
61
- self._grouped = len(self.sample_ids) > 0 and isinstance(self.sample_ids[0], list)
62
- if self._grouped:
63
- groups = cast(List[List[SampleId]], self.sample_ids)
64
- assert len(groups) >= 1, "Grouped PreprocessResponse must have at least one group."
65
- for group in groups:
66
- assert isinstance(group, list) and len(group) > 0, \
67
- "Each group in a grouped PreprocessResponse must be a non-empty list."
68
- self.sample_id_type = type(groups[0][0])
69
- for group in groups:
70
- for sample_id in group:
71
- assert isinstance(sample_id, self.sample_id_type), \
72
- f"Sample id should be of type {self.sample_id_type.__name__}. Got: {type(sample_id)}"
73
- self.length = sum(len(group) for group in groups)
74
- else:
75
- flat_ids = cast(List[SampleId], self.sample_ids)
76
- self.length = len(flat_ids)
77
- if self.sample_id_type is None:
78
- self.sample_id_type = str
79
- if self.sample_id_type == str:
80
- for sample_id in flat_ids:
81
- assert isinstance(sample_id, str), f"Sample id should be of type str. Got: {type(sample_id)}"
57
+ self.length = len(self.sample_ids)
58
+ if self.sample_id_type is None:
59
+ self.sample_id_type = str
60
+ if self.sample_id_type == str:
61
+ for sample_id in self.sample_ids:
62
+ assert isinstance(sample_id, str), f"Sample id should be of type str. Got: {type(sample_id)}"
82
63
  else:
83
64
  raise Exception("length is deprecated, please use sample_ids instead.")
84
65
 
@@ -98,36 +79,9 @@ class PreprocessResponse:
98
79
  return id(self)
99
80
 
100
81
  def __len__(self) -> int:
101
- assert self.length is not None
102
- return self.length
103
-
104
- @property
105
- def is_grouped(self) -> bool:
106
- return self._grouped
107
-
108
- @property
109
- def num_groups(self) -> int:
110
- # Grouped: the number of groups. Flat: the (flat) sample count, since sample_ids is the
111
- # flat id list and there is one sample per "group".
112
82
  assert self.sample_ids is not None
113
83
  return len(self.sample_ids)
114
84
 
115
- @property
116
- def groups(self) -> Optional[List[List[SampleId]]]:
117
- # The nested list of groups when grouped, otherwise None. Callers should
118
- # branch on is_grouped before relying on this.
119
- return cast(List[List[SampleId]], self.sample_ids) if self._grouped else None
120
-
121
- @property
122
- def flat_sample_ids(self) -> List[SampleId]:
123
- # All sample ids in order: concatenation of the groups when grouped,
124
- # otherwise the flat sample_ids list as-is.
125
- assert self.sample_ids is not None
126
- if self._grouped:
127
- groups = cast(List[List[SampleId]], self.sample_ids)
128
- return [sample_id for group in groups for sample_id in group]
129
- return cast(List[SampleId], self.sample_ids)
130
-
131
85
  @dataclass
132
86
  class ElementInstance:
133
87
  name: str
@@ -138,15 +92,19 @@ SectionCallableInterface = Callable[[Union[int, str], PreprocessResponse], npt.N
138
92
  InstanceCallableInterface = Callable[[Union[int, str], PreprocessResponse, int], Optional[ElementInstance]]
139
93
  InstanceLengthCallableInterface = Callable[[Union[int, str], PreprocessResponse], int]
140
94
 
141
- # (sample_id, prev_inputs, prev_outputs, preprocess) -> next model inputs, or None to end the chain.
142
- # First call per chain receives prev_inputs=None, prev_outputs=None and returns the initial inputs.
143
- # Keys starting with '_' are passthrough state: never fed to the model, handed back verbatim in
144
- # prev_inputs on the next call. The hook must be stateless and deterministically seeded (the engine
145
- # may replay a step after crash recovery and relies on identical results).
95
+ # (sample_id, prev_inputs, prev_outputs, state, preprocess) -> (next model inputs | None, state).
96
+ # First call per chain receives prev_inputs=None, prev_outputs=None, state=None and returns the
97
+ # initial model inputs plus the initial state. Returning next_inputs=None ends the chain the
98
+ # terminating call still returns state, so the final step participates in state aggregation.
99
+ # State is an arbitrary nest of dicts/lists holding numpy arrays, numbers, strings or bools; it is
100
+ # never fed to the model and rides the engine's redis queue on every step, so accumulate
101
+ # reductions, not raw per-step tensors. The hook must be stateless and deterministically seeded
102
+ # (the engine may replay a step after crash recovery and relies on identical results).
103
+ AutoregressiveChainState = Any
146
104
  AutoregressiveStepCallableInterface = Callable[
147
105
  [Union[int, str], Optional[Dict[str, npt.NDArray[np.float32]]], Optional[Dict[str, npt.NDArray[np.float32]]],
148
- PreprocessResponse],
149
- Optional[Dict[str, npt.NDArray[np.float32]]]]
106
+ AutoregressiveChainState, PreprocessResponse],
107
+ Tuple[Optional[Dict[str, npt.NDArray[np.float32]]], AutoregressiveChainState]]
150
108
 
151
109
  MetadataSectionCallableInterface = Union[
152
110
  Callable[[Union[int, str], PreprocessResponse], int],
@@ -302,16 +260,42 @@ class CustomLatentSpaceHandler:
302
260
  # dominates the model inputs). 'mean': every latent space is the elementwise mean over all steps.
303
261
  AUTOREGRESSIVE_LATENT_SPACE_AGGREGATIONS = ('last_step', 'mean')
304
262
 
263
+ # Reserved argument names of autoregressive metrics/losses/visualizers, fed implicitly by the
264
+ # platform from the finished chain (per-chain dicts, no batch axis). Any other argument is wired
265
+ # to a ground-truth encoder through the integration test.
266
+ AUTOREGRESSIVE_IMPLICIT_ARG_NAMES = ('inputs', 'outputs', 'state')
267
+
305
268
 
306
269
  @dataclass
307
270
  class AutoregressiveStepHandler:
308
271
  function: AutoregressiveStepCallableInterface
309
272
  name: str = 'autoregressive_step'
310
- # Per-model-input shapes (unbatched, underscore passthrough keys excluded), discovered by running
311
- # the hook's first call at parse time. Fills the role InputHandler.shape plays for input encoders.
273
+ # Per-model-input shapes (unbatched), discovered by running the hook's first call at parse
274
+ # time. Fills the role InputHandler.shape plays for input encoders.
312
275
  input_shapes: Optional[Dict[str, List[int]]] = None
313
276
  latent_space_aggregation: str = 'last_step'
314
277
 
278
+
279
+ # Per-chain, unbatched callables: called once per finished chain with the final step's tensors —
280
+ # inputs/outputs/state are fed implicitly by the platform (dicts, no batch axis; outputs keyed by
281
+ # prediction-type names), remaining args are wired ground-truth encoder results.
282
+ @dataclass
283
+ class AutoregressiveMetricHandler:
284
+ metric_handler_data: MetricHandlerData
285
+ function: CustomCallableInterfaceMultiArgs
286
+
287
+
288
+ @dataclass
289
+ class AutoregressiveLossHandler:
290
+ custom_loss_handler_data: CustomLossHandlerData
291
+ function: CustomCallableInterface
292
+
293
+
294
+ @dataclass
295
+ class AutoregressiveVisualizerHandler:
296
+ visualizer_handler_data: VisualizerHandlerData
297
+ function: VisualizerCallableInterface
298
+
315
299
  @dataclass
316
300
  class PredictionTypeHandler:
317
301
  name: str
@@ -350,9 +334,12 @@ class DatasetIntegrationSetup:
350
334
  metrics: List[MetricHandler] = field(default_factory=list)
351
335
  instance_metrics: List[InstanceMetricHandler] = field(default_factory=list)
352
336
  custom_layers: Dict[str, CustomLayerHandler] = field(default_factory=dict)
353
- custom_latent_space: Optional[CustomLatentSpaceHandler] = None
337
+ custom_latent_spaces: Dict[str, CustomLatentSpaceHandler] = field(default_factory=dict)
354
338
  simulations: List[SimulationHandler] = field(default_factory=list)
355
339
  autoregressive_step: Optional[AutoregressiveStepHandler] = None
340
+ autoregressive_metrics: List[AutoregressiveMetricHandler] = field(default_factory=list)
341
+ autoregressive_losses: List[AutoregressiveLossHandler] = field(default_factory=list)
342
+ autoregressive_visualizers: List[AutoregressiveVisualizerHandler] = field(default_factory=list)
356
343
 
357
344
 
358
345
  @dataclass
@@ -363,6 +350,6 @@ class DatasetSample:
363
350
  metadata_is_none: Dict[str, bool]
364
351
  index: Union[int, str]
365
352
  state: DataStateEnum
366
- custom_latent_space: Optional[npt.NDArray[np.float32]] = None
353
+ custom_latent_spaces: Optional[Dict[str, npt.NDArray[np.float32]]] = None
367
354
  instance_masks: Optional[Dict[str, ElementInstance]] = None
368
355
 
@@ -1,6 +1,6 @@
1
1
 
2
2
  import inspect
3
- from typing import Callable, List, Optional, Dict, Any, Type, Union, get_args, cast
3
+ from typing import Callable, List, Optional, Dict, Any, Type, Union, get_args
4
4
 
5
5
  import numpy as np
6
6
  import numpy.typing as npt
@@ -14,7 +14,8 @@ from code_loader.contract.datasetclasses import SectionCallableInterface, InputH
14
14
  RawInputsForHeatmap, VisualizerHandlerData, MetricHandlerData, CustomLossHandlerData, SamplePreprocessResponse, \
15
15
  ElementInstanceMasksHandler, InstanceCallableInterface, CustomLatentSpaceHandler, InstanceMetricHandler, \
16
16
  SimulationHandler, _simulation_context, AutoregressiveStepHandler, AutoregressiveStepCallableInterface, \
17
- AUTOREGRESSIVE_LATENT_SPACE_AGGREGATIONS
17
+ AUTOREGRESSIVE_LATENT_SPACE_AGGREGATIONS, AUTOREGRESSIVE_IMPLICIT_ARG_NAMES, \
18
+ AutoregressiveMetricHandler, AutoregressiveLossHandler, AutoregressiveVisualizerHandler
18
19
  from code_loader.contract.enums import LeapDataType, DataStateEnum, DataStateType, MetricDirection, DatasetMetadataType
19
20
  from code_loader.contract.mapping import NodeConnection, NodeMapping, NodeMappingType
20
21
  from code_loader.contract.responsedataclasses import DatasetTestResultPayload, LeapAnalysisConfiguration
@@ -512,18 +513,34 @@ class LeapBinder:
512
513
  """
513
514
  self.setup_container.metadata.append(MetadataHandler(name, function, metadata_type))
514
515
 
515
- def set_custom_latent_space(self, function: SectionCallableInterface) -> None:
516
+ def set_custom_latent_space(self, function: SectionCallableInterface,
517
+ name: Optional[str] = None) -> None:
516
518
  """
517
- Set a custom latent space function.
519
+ Register a custom latent space function.
520
+
521
+ Multiple custom latent spaces may be registered as long as each has a
522
+ unique name. They are stored name-keyed (insertion order preserved), so a
523
+ later registration no longer overrides an earlier one.
518
524
 
519
525
  Args:
520
- function (SectionCallableInterface): The metadata handler function.
526
+ function (SectionCallableInterface): The latent-space handler function.
521
527
  This function receives:
522
528
  subset (PreprocessResponse): The subset of the data.
523
529
  index (int): The index of the sample within the subset.
524
- This function should numpy float32 array contains the latent space vec of the sample.
530
+ This function should return a numpy float32 array containing the latent
531
+ space vec of the sample.
532
+ name (Optional[str]): Unique name for this custom latent space. Defaults to
533
+ the reserved single-LS name for backward compatibility.
525
534
  """
526
- self.setup_container.custom_latent_space = CustomLatentSpaceHandler(function)
535
+ if name is None:
536
+ name = custom_latent_space_attribute
537
+ if name in self.setup_container.custom_latent_spaces:
538
+ raise Exception(
539
+ f"A custom latent space named '{name}' is already registered. Each "
540
+ f"@tensorleap_custom_latent_space must have a unique name "
541
+ f"(pass name='...' to distinguish them)."
542
+ )
543
+ self.setup_container.custom_latent_spaces[name] = CustomLatentSpaceHandler(function, name)
527
544
 
528
545
  def set_autoregressive_step(self, function: AutoregressiveStepCallableInterface,
529
546
  latent_space_aggregation: str = 'last_step') -> None:
@@ -553,6 +570,83 @@ class LeapBinder:
553
570
 
554
571
  self.set_metadata(builtin_chain_metadata, 'builtin_chain')
555
572
 
573
+ def _autoregressive_arg_names(self, function: Callable[..., Any], decorator_name: str,
574
+ name: str) -> List[str]:
575
+ """Split an autoregressive callable's signature into implicit chain args (validated
576
+ present) and wired arg names (returned) — the wired args connect to ground-truth
577
+ encoders through the integration test."""
578
+ argspec = inspect.getfullargspec(function)
579
+ for arg_name, arg_type in argspec.annotations.items():
580
+ _reject_stringized_sample_preprocess_response(function, arg_name, arg_type)
581
+ if arg_type == SamplePreprocessResponse:
582
+ raise Exception(
583
+ f'{decorator_name} "{name}": SamplePreprocessResponse arguments are not '
584
+ f'supported for autoregressive decorators yet. Derive what you need inside '
585
+ f'the autoregressive step hook and carry it in the state.')
586
+ implicit = [arg_name for arg_name in argspec[0]
587
+ if arg_name in AUTOREGRESSIVE_IMPLICIT_ARG_NAMES]
588
+ if not implicit:
589
+ raise Exception(
590
+ f'{decorator_name} "{name}": the function must declare at least one of the '
591
+ f'implicit chain arguments {list(AUTOREGRESSIVE_IMPLICIT_ARG_NAMES)} — they are '
592
+ f'fed by the platform from the finished chain.')
593
+ return [arg_name for arg_name in argspec[0]
594
+ if arg_name not in AUTOREGRESSIVE_IMPLICIT_ARG_NAMES]
595
+
596
+ def _assert_unique_metric_name(self, name: str) -> None:
597
+ existing = [handler.metric_handler_data.name for handler in self.setup_container.metrics] \
598
+ + [handler.metric_handler_data.name
599
+ for handler in self.setup_container.autoregressive_metrics]
600
+ if name in existing:
601
+ raise Exception(f'Metric with name {name} already exists. Please choose another')
602
+
603
+ def _assert_unique_loss_name(self, name: str) -> None:
604
+ existing = [handler.custom_loss_handler_data.name
605
+ for handler in self.setup_container.custom_loss_handlers] \
606
+ + [handler.custom_loss_handler_data.name
607
+ for handler in self.setup_container.autoregressive_losses]
608
+ if name in existing:
609
+ raise Exception(f'Custom loss with name {name} already exists. Please choose another')
610
+
611
+ def _assert_unique_visualizer_name(self, name: str) -> None:
612
+ existing = [handler.visualizer_handler_data.name
613
+ for handler in self.setup_container.visualizers] \
614
+ + [handler.visualizer_handler_data.name
615
+ for handler in self.setup_container.autoregressive_visualizers]
616
+ if name in existing:
617
+ raise Exception(f'Visualizer with name {name} already exists. Please choose another')
618
+
619
+ def add_autoregressive_metric(self, function: CustomCallableInterfaceMultiArgs, name: str,
620
+ direction: Optional[Union[MetricDirection, Dict[str, MetricDirection]]]
621
+ = MetricDirection.Downward,
622
+ compute_insights: Optional[Union[bool, Dict[str, bool]]] = None) -> None:
623
+ self._assert_unique_metric_name(name)
624
+ arg_names = self._autoregressive_arg_names(function, 'tensorleap_autoregressive_metric',
625
+ name)
626
+ metric_handler_data = MetricHandlerData(name, arg_names, direction, compute_insights)
627
+ self.setup_container.autoregressive_metrics.append(
628
+ AutoregressiveMetricHandler(metric_handler_data, function))
629
+
630
+ def add_autoregressive_loss(self, function: CustomCallableInterface, name: str) -> None:
631
+ self._assert_unique_loss_name(name)
632
+ arg_names = self._autoregressive_arg_names(function, 'tensorleap_autoregressive_loss',
633
+ name)
634
+ self.setup_container.autoregressive_losses.append(
635
+ AutoregressiveLossHandler(CustomLossHandlerData(name, arg_names), function))
636
+
637
+ def add_autoregressive_visualizer(self, function: VisualizerCallableInterface, name: str,
638
+ visualizer_type: LeapDataType) -> None:
639
+ self._assert_unique_visualizer_name(name)
640
+ if visualizer_type.value not in map_leap_data_type_to_visualizer_class:
641
+ raise Exception(
642
+ f'The visualizer_type is invalid. current visualizer_type: {visualizer_type}, '
643
+ f'should be one of : {", ".join([arg.__name__ for arg in get_args(LeapData)])}')
644
+ arg_names = self._autoregressive_arg_names(
645
+ function, 'tensorleap_autoregressive_visualizer', name)
646
+ self.setup_container.autoregressive_visualizers.append(
647
+ AutoregressiveVisualizerHandler(VisualizerHandlerData(name, visualizer_type,
648
+ arg_names), function))
649
+
556
650
  def set_custom_layer(self, custom_layer: Type[Any], name: str, inspect_layer: bool = False,
557
651
  kernel_index: Optional[int] = None, use_custom_latent_space: bool = False) -> None:
558
652
  """
@@ -635,15 +729,6 @@ class LeapBinder:
635
729
  if state_enum in preprocess_result_dict:
636
730
  preprocess_result_dict_in_correct_order[state_enum] = preprocess_result_dict[state_enum]
637
731
 
638
- # All-or-none: every state must agree on grouped (nested sample_ids) vs flat.
639
- if len({r.is_grouped for r in preprocess_result_dict_in_correct_order.values()}) > 1:
640
- shapes = ', '.join(
641
- f"{state.name}={'grouped' if r.is_grouped else 'flat'}"
642
- for state, r in preprocess_result_dict_in_correct_order.items())
643
- raise Exception(
644
- "All preprocess states must be either all grouped (nested sample_ids) or all flat, "
645
- f"but got a mix: {shapes}.")
646
-
647
732
  return preprocess_result_dict_in_correct_order
648
733
 
649
734
  def get_preprocess_unlabeled_result(self) -> Optional[PreprocessResponse]:
@@ -664,15 +749,7 @@ class LeapBinder:
664
749
  preprocess_response: PreprocessResponse, test_result: List[DatasetTestResultPayload],
665
750
  dataset_base_handler: Union[DatasetBaseHandler, MetadataHandler], state: DataStateEnum) -> List[DatasetTestResultPayload]:
666
751
  assert preprocess_response.sample_ids is not None
667
- if preprocess_response.is_grouped:
668
- # Grouped: probe with the first group, then reduce to a single sample's result so the
669
- # recorded shape/type is per-sample (matching the flat case), not the (B, *) batch.
670
- # (casts: sample_ids[0] is a group list here, and the grouped result is a per-sample list.)
671
- group = preprocess_response.sample_ids[0]
672
- raw_result = cast(Any, dataset_base_handler.function(cast(Any, group), preprocess_response))[0]
673
- else:
674
- raw_result = dataset_base_handler.function(
675
- cast(Union[int, str], preprocess_response.sample_ids[0]), preprocess_response)
752
+ raw_result = dataset_base_handler.function(preprocess_response.sample_ids[0], preprocess_response)
676
753
  handler_type = 'metadata' if isinstance(dataset_base_handler, MetadataHandler) else None
677
754
  if isinstance(dataset_base_handler, MetadataHandler):
678
755
  if isinstance(raw_result, dict):
@@ -768,11 +845,10 @@ class LeapBinder:
768
845
  preprocess_response.tl_generated = True
769
846
  if not preprocess_response.length or preprocess_response.length < 1:
770
847
  raise Exception("Simulation '{}' returned PreprocessResponse with length < 1".format(sim.name))
771
- preprocess_response.sample_ids = cast(List[Union[int, str]], [0])
772
- sim_sample_ids = cast(List[Union[int, str]], preprocess_response.sample_ids)
848
+ preprocess_response.sample_ids = [0]
773
849
  for handler in self.setup_container.inputs:
774
- out1 = handler.function(sim_sample_ids[0], preprocess_response)
775
- out2 = handler.function(sim_sample_ids[0], preprocess_response)
850
+ out1 = handler.function(preprocess_response.sample_ids[0], preprocess_response)
851
+ out2 = handler.function(preprocess_response.sample_ids[0], preprocess_response)
776
852
  if not np.allclose(out1, out2):
777
853
  raise Exception(
778
854
  "Simulation '{}': encoder '{}' is non-deterministic — consecutive calls with seed=0 returned different outputs".format(
@@ -796,6 +872,23 @@ class LeapBinder:
796
872
  appear in any order in the integration file.
797
873
  """
798
874
  if self.setup_container.autoregressive_step is None:
875
+ declared_autoregressive = [
876
+ ('tensorleap_autoregressive_metric',
877
+ [handler.metric_handler_data.name
878
+ for handler in self.setup_container.autoregressive_metrics]),
879
+ ('tensorleap_autoregressive_loss',
880
+ [handler.custom_loss_handler_data.name
881
+ for handler in self.setup_container.autoregressive_losses]),
882
+ ('tensorleap_autoregressive_visualizer',
883
+ [handler.visualizer_handler_data.name
884
+ for handler in self.setup_container.autoregressive_visualizers]),
885
+ ]
886
+ for decorator_name, names in declared_autoregressive:
887
+ if names:
888
+ raise Exception(
889
+ f'{decorator_name} {names} requires a tensorleap_autoregressive_step '
890
+ f'hook — autoregressive decorators consume the finished chain the hook '
891
+ f'drives.')
799
892
  return
800
893
  if self.setup_container.inputs:
801
894
  raise Exception(