code-loader 1.0.197.dev1__py3-none-any.whl → 1.0.197.dev2__py3-none-any.whl

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.
@@ -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, Tuple
3
+ from typing import Any, Callable, List, Optional, Dict, Union, Type, Literal, Tuple, cast
4
4
  import re
5
5
  import numpy as np
6
6
  import numpy.typing as npt
@@ -15,6 +15,8 @@ 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
+
18
20
 
19
21
  @dataclass
20
22
  class PreprocessResponse:
@@ -39,27 +41,54 @@ class PreprocessResponse:
39
41
  """
40
42
  length: Optional[int] = None # Deprecated. Please use sample_ids instead
41
43
  data: Any = None
42
- sample_ids: Optional[Union[List[str], List[int]]] = None
44
+ sample_ids: Optional[Union[List[SampleId], List[List[SampleId]]]] = None
43
45
  state: Optional[DataStateType] = None
44
46
  sample_id_type: Optional[Union[Type[str], Type[int]]] = None
45
47
  sample_ids_to_instance_mappings: Optional[Dict[str, List[str]]] = None # in use only for element instance
46
48
  instance_to_sample_ids_mappings: Optional[Dict[str, str]] = None # in use only for element instance
47
49
  tl_generated: bool = False
50
+ _grouped: bool = field(default=False, init=False, repr=False, compare=False)
51
+ _group_pos_cache: Optional[Dict[SampleId, Tuple[List[SampleId], int]]] = field(
52
+ default=None, init=False, repr=False, compare=False)
48
53
 
49
54
  def __post_init__(self) -> None:
50
55
  assert self.sample_ids_to_instance_mappings is None, f"Keep sample_ids_to_instance_mappings None when initializing PreprocessResponse"
51
56
  assert self.instance_to_sample_ids_mappings is None, f"Keep instance_to_sample_ids_mappings None when initializing PreprocessResponse"
52
57
 
53
58
  if self.length is not None and self.sample_ids is None:
54
- self.sample_ids = [i for i in range(self.length)]
59
+ self.sample_ids = cast(List[SampleId], [i for i in range(self.length)])
55
60
  self.sample_id_type = int
56
- elif self.length is None and self.sample_ids is not None:
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)}"
61
+ self._grouped = False
62
+ elif self.sample_ids is not None:
63
+ given_length = self.length
64
+ self._grouped = len(self.sample_ids) > 0 and isinstance(self.sample_ids[0], list)
65
+ if self._grouped:
66
+ groups = cast(List[List[SampleId]], self.sample_ids)
67
+ assert len(groups) >= 1, "Grouped PreprocessResponse must have at least one group."
68
+ for group in groups:
69
+ assert isinstance(group, list) and len(group) > 0, \
70
+ "Each group in a grouped PreprocessResponse must be a non-empty list."
71
+ self.sample_id_type = type(groups[0][0])
72
+ assert issubclass(self.sample_id_type, (str, int)), \
73
+ f"Sample id should be of type str or int. Got: {self.sample_id_type.__name__}"
74
+ for group in groups:
75
+ for sample_id in group:
76
+ assert isinstance(sample_id, self.sample_id_type), \
77
+ f"Sample id should be of type {self.sample_id_type.__name__}. Got: {type(sample_id)}"
78
+ self.length = sum(len(group) for group in groups)
79
+ else:
80
+ flat_ids = cast(List[SampleId], self.sample_ids)
81
+ self.length = len(flat_ids)
82
+ if self.sample_id_type is None:
83
+ self.sample_id_type = str
84
+ if self.sample_id_type == str:
85
+ for sample_id in flat_ids:
86
+ assert isinstance(sample_id, str), f"Sample id should be of type str. Got: {type(sample_id)}"
87
+ # dataclasses.replace() re-invokes __init__ with both fields already populated
88
+ # (length previously derived from sample_ids); only reject a genuine mismatch.
89
+ if given_length is not None:
90
+ assert given_length == self.length, \
91
+ f"Inconsistent PreprocessResponse: length={given_length} but sample_ids implies {self.length}."
63
92
  else:
64
93
  raise Exception("length is deprecated, please use sample_ids instead.")
65
94
 
@@ -79,9 +108,36 @@ class PreprocessResponse:
79
108
  return id(self)
80
109
 
81
110
  def __len__(self) -> int:
111
+ assert self.length is not None
112
+ return self.length
113
+
114
+ @property
115
+ def is_grouped(self) -> bool:
116
+ return self._grouped
117
+
118
+ @property
119
+ def num_groups(self) -> int:
120
+ # Grouped: the number of groups. Flat: the (flat) sample count, since sample_ids is the
121
+ # flat id list and there is one sample per "group".
82
122
  assert self.sample_ids is not None
83
123
  return len(self.sample_ids)
84
124
 
125
+ @property
126
+ def groups(self) -> Optional[List[List[SampleId]]]:
127
+ # The nested list of groups when grouped, otherwise None. Callers should
128
+ # branch on is_grouped before relying on this.
129
+ return cast(List[List[SampleId]], self.sample_ids) if self._grouped else None
130
+
131
+ @property
132
+ def flat_sample_ids(self) -> List[SampleId]:
133
+ # All sample ids in order: concatenation of the groups when grouped,
134
+ # otherwise the flat sample_ids list as-is.
135
+ assert self.sample_ids is not None
136
+ if self._grouped:
137
+ groups = cast(List[List[SampleId]], self.sample_ids)
138
+ return [sample_id for group in groups for sample_id in group]
139
+ return cast(List[SampleId], self.sample_ids)
140
+
85
141
  @dataclass
86
142
  class ElementInstance:
87
143
  name: str
@@ -1,6 +1,9 @@
1
1
 
2
+ import builtins
2
3
  import inspect
3
- from typing import Callable, List, Optional, Dict, Any, Type, Union, get_args
4
+ import os
5
+ from contextlib import contextmanager
6
+ from typing import Callable, List, Optional, Dict, Any, Type, Union, get_args, cast, Iterator, Set
4
7
 
5
8
  import numpy as np
6
9
  import numpy.typing as npt
@@ -16,7 +19,8 @@ from code_loader.contract.datasetclasses import SectionCallableInterface, InputH
16
19
  SimulationHandler, _simulation_context, AutoregressiveStepHandler, AutoregressiveStepCallableInterface, \
17
20
  AUTOREGRESSIVE_LATENT_SPACE_AGGREGATIONS, AUTOREGRESSIVE_IMPLICIT_ARG_NAMES, \
18
21
  AutoregressiveMetricHandler, AutoregressiveLossHandler, AutoregressiveVisualizerHandler
19
- from code_loader.contract.enums import LeapDataType, DataStateEnum, DataStateType, MetricDirection, DatasetMetadataType
22
+ from code_loader.contract.enums import LeapDataType, DataStateEnum, DataStateType, MetricDirection, DatasetMetadataType, \
23
+ TestingSectionEnum
20
24
  from code_loader.contract.mapping import NodeConnection, NodeMapping, NodeMappingType
21
25
  from code_loader.contract.responsedataclasses import DatasetTestResultPayload, LeapAnalysisConfiguration
22
26
  from code_loader.contract.visualizer_classes import map_leap_data_type_to_visualizer_class
@@ -33,6 +37,25 @@ from code_loader.visualizers.default_visualizers import DefaultVisualizer, \
33
37
  mapping_runtime_mode_env_var_mame = '__MAPPING_RUNTIME_MODE__'
34
38
 
35
39
 
40
+ @contextmanager
41
+ def _track_opened_files() -> Iterator[Set[str]]:
42
+ opened: Set[str] = set()
43
+ original_open = builtins.open
44
+
45
+ def tracking_open(file: Any, *args: Any, **kwargs: Any) -> Any:
46
+ mode = args[0] if args else kwargs.get('mode', 'r')
47
+ is_read_mode = isinstance(mode, str) and not any(c in mode for c in ('a', 'w', 'x'))
48
+ if is_read_mode and isinstance(file, (str, os.PathLike)):
49
+ opened.add(os.fspath(file))
50
+ return original_open(file, *args, **kwargs)
51
+
52
+ builtins.open = tracking_open
53
+ try:
54
+ yield opened
55
+ finally:
56
+ builtins.open = original_open
57
+
58
+
36
59
  def _stringized_annotation_type_name(annotation: Any) -> Optional[str]:
37
60
  """Bare type name if ``annotation`` is a stringized annotation (from ``from __future__
38
61
  import annotations`` or a quoted hint), else ``None``. code_loader inspects raw
@@ -585,7 +608,9 @@ class LeapBinder:
585
608
  # Builtin chain metadata, declared at parse time so it survives the reporter's
586
609
  # metadata type mapping; the placeholder values are overwritten by the engine when a
587
610
  # chain finalizes (realized length, truncated-by-safety-cap flag).
588
- def builtin_chain_metadata(idx: Any, preprocess: PreprocessResponse) -> Dict[str, Any]:
611
+ def builtin_chain_metadata(idx: Any, preprocess: PreprocessResponse) -> Any:
612
+ if isinstance(idx, list):
613
+ return [{'length': -1, 'truncated': False} for _ in idx]
589
614
  return {'length': -1, 'truncated': False}
590
615
 
591
616
  self.set_metadata(builtin_chain_metadata, 'builtin_chain')
@@ -749,6 +774,15 @@ class LeapBinder:
749
774
  if state_enum in preprocess_result_dict:
750
775
  preprocess_result_dict_in_correct_order[state_enum] = preprocess_result_dict[state_enum]
751
776
 
777
+ # All-or-none: every state must agree on grouped (nested sample_ids) vs flat.
778
+ if len({r.is_grouped for r in preprocess_result_dict_in_correct_order.values()}) > 1:
779
+ shapes = ', '.join(
780
+ f"{state.name}={'grouped' if r.is_grouped else 'flat'}"
781
+ for state, r in preprocess_result_dict_in_correct_order.items())
782
+ raise Exception(
783
+ "All preprocess states must be either all grouped (nested sample_ids) or all flat, "
784
+ f"but got a mix: {shapes}.")
785
+
752
786
  return preprocess_result_dict_in_correct_order
753
787
 
754
788
  def get_preprocess_unlabeled_result(self) -> Optional[PreprocessResponse]:
@@ -769,7 +803,34 @@ class LeapBinder:
769
803
  preprocess_response: PreprocessResponse, test_result: List[DatasetTestResultPayload],
770
804
  dataset_base_handler: Union[DatasetBaseHandler, MetadataHandler], state: DataStateEnum) -> List[DatasetTestResultPayload]:
771
805
  assert preprocess_response.sample_ids is not None
772
- raw_result = dataset_base_handler.function(preprocess_response.sample_ids[0], preprocess_response)
806
+ opened_files: Optional[Set[str]] = None
807
+ if preprocess_response.is_grouped:
808
+ # Grouped: probe with the first group, then reduce to a single sample's result so the
809
+ # recorded shape/type is per-sample (matching the flat case), not the (B, *) batch.
810
+ # (casts: sample_ids[0] is a group list here, and the grouped result is a per-sample list.)
811
+ group = preprocess_response.sample_ids[0]
812
+ with _track_opened_files() as opened_files:
813
+ raw_result = cast(Any, dataset_base_handler.function(cast(Any, group), preprocess_response))[0]
814
+ else:
815
+ raw_result = dataset_base_handler.function(
816
+ cast(Union[int, str], preprocess_response.sample_ids[0]), preprocess_response)
817
+
818
+ def _warn_if_group_spans_multiple_files(payloads: List[DatasetTestResultPayload]) -> None:
819
+ if opened_files is None or len(opened_files) <= 1:
820
+ return
821
+ shown = sorted(opened_files)[:5]
822
+ suffix = f" (+{len(opened_files) - 5} more)" if len(opened_files) > 5 else ""
823
+ warning = (
824
+ f"Encoding declared group 0 (size {len(cast(List[Any], group))}) for '{dataset_base_handler.name}' opened "
825
+ f"{len(opened_files)} distinct files: {shown}{suffix}. If this group is meant to be one "
826
+ f"physical source (e.g. one Parquet file), it may be grouped incorrectly - check your "
827
+ f"preprocess grouping logic. (Only files opened via Python's open() are tracked, so this "
828
+ f"can miss other I/O paths or be a false positive if grouping isn't meant to reflect file "
829
+ f"locality.)")
830
+ for payload in payloads:
831
+ existing = payload.display.get(TestingSectionEnum.Errors.name, '')
832
+ payload.display[TestingSectionEnum.Errors.name] = (existing + '\n' if existing else '') + warning
833
+
773
834
  handler_type = 'metadata' if isinstance(dataset_base_handler, MetadataHandler) else None
774
835
  if isinstance(dataset_base_handler, MetadataHandler):
775
836
  if isinstance(raw_result, dict):
@@ -809,6 +870,7 @@ class LeapBinder:
809
870
  else:
810
871
  if raw_result is None:
811
872
  if state != DataStateEnum.training:
873
+ _warn_if_group_spans_multiple_files(test_result)
812
874
  return test_result
813
875
 
814
876
  if dataset_base_handler.metadata_type is None:
@@ -830,6 +892,7 @@ class LeapBinder:
830
892
  # setting shape in setup for all encoders
831
893
  if isinstance(dataset_base_handler, (InputHandler, GroundTruthHandler)):
832
894
  dataset_base_handler.shape = result_shape
895
+ _warn_if_group_spans_multiple_files(test_result)
833
896
  return test_result
834
897
 
835
898
  def check_handlers(self, preprocess_result: Dict[DataStateEnum, PreprocessResponse]) -> None:
@@ -865,10 +928,15 @@ class LeapBinder:
865
928
  preprocess_response.tl_generated = True
866
929
  if not preprocess_response.length or preprocess_response.length < 1:
867
930
  raise Exception("Simulation '{}' returned PreprocessResponse with length < 1".format(sim.name))
868
- preprocess_response.sample_ids = [0]
931
+ if preprocess_response.is_grouped:
932
+ raise Exception(
933
+ "Simulation '{}' returned a grouped PreprocessResponse; simulations must "
934
+ "return a flat (non-grouped) PreprocessResponse.".format(sim.name))
935
+ preprocess_response.sample_ids = cast(List[Union[int, str]], [0])
936
+ sim_sample_ids = cast(List[Union[int, str]], preprocess_response.sample_ids)
869
937
  for handler in self.setup_container.inputs:
870
- out1 = handler.function(preprocess_response.sample_ids[0], preprocess_response)
871
- out2 = handler.function(preprocess_response.sample_ids[0], preprocess_response)
938
+ out1 = handler.function(sim_sample_ids[0], preprocess_response)
939
+ out2 = handler.function(sim_sample_ids[0], preprocess_response)
872
940
  if not np.allclose(out1, out2):
873
941
  raise Exception(
874
942
  "Simulation '{}': encoder '{}' is non-deterministic — consecutive calls with seed=0 returned different outputs".format(
@@ -40,6 +40,7 @@ _called_from_inside_tl_integration_test_decorator = False
40
40
  # to tell a raise in the test body (report as code-flow failure) from a module-level crash
41
41
  # before any test ran (report as a plain script crash).
42
42
  _integration_test_started = False
43
+ _mapping_dataset_is_grouped = False
43
44
  _call_from_tl_platform = os.environ.get('IS_TENSORLEAP_PLATFORM') == 'true'
44
45
 
45
46
  # ---- warnings store (module-level) ----
@@ -213,6 +214,53 @@ def batch_warning(result, func_name):
213
214
  )
214
215
 
215
216
 
217
+ def _validate_id_or_group(sample_id, preprocess_response, func_name):
218
+ """Accept either a single sample id (flat dataset) or a group — a list of ids (grouped
219
+ dataset). The argument shape must match the dataset: a group requires a grouped
220
+ PreprocessResponse and a single id requires a flat one. Every id must match the declared
221
+ sample_id_type."""
222
+ is_group = isinstance(sample_id, list)
223
+ if is_group and not preprocess_response.is_grouped:
224
+ raise AssertionError(
225
+ f'{func_name}() validation failed: got a group (list of sample ids) but the '
226
+ f'PreprocessResponse is not grouped. Pass a single sample id for a flat dataset.')
227
+ if not is_group and preprocess_response.is_grouped:
228
+ raise AssertionError(
229
+ f'{func_name}() validation failed: got a single sample id but the PreprocessResponse '
230
+ f'is grouped. Pass a group (list of sample ids) for a grouped dataset.')
231
+ ids = sample_id if is_group else [sample_id]
232
+ for sid in ids:
233
+ assert type(sid) == preprocess_response.sample_id_type, \
234
+ (f'{func_name}() validation failed: '
235
+ f'Argument sample_id should be as the same type as defined in the preprocess response '
236
+ f'{preprocess_response.sample_id_type}. Got {type(sid)}.')
237
+
238
+
239
+ def _validate_grouped_result(result, group_size, func_name, validate_single):
240
+ """A grouped (group) encoder result must be either a single ndarray with leading dim
241
+ == group_size, or a list of `group_size` per-sample results (each validated by
242
+ validate_single). Metadata validates its grouped result separately (list only).
243
+ An `(B, *)` array is validated per-sample slice (not as the whole batched array) so
244
+ per-sample checks (e.g. channel_dim <= rank) use the sample shape, not the batch shape."""
245
+ if isinstance(result, np.ndarray):
246
+ assert len(result.shape) > 0 and result.shape[0] == group_size, \
247
+ (f'{func_name}() validation failed: expected a grouped output for a group of '
248
+ f'{group_size}: an array with leading dim {group_size}, got shape {result.shape}.')
249
+ for single in result:
250
+ validate_single(single)
251
+ elif isinstance(result, list):
252
+ assert len(result) == group_size, \
253
+ (f'{func_name}() validation failed: expected a grouped output for a group of '
254
+ f'{group_size}: a list of {group_size} arrays, got {len(result)}.')
255
+ for r in result:
256
+ validate_single(r)
257
+ else:
258
+ raise AssertionError(
259
+ f'{func_name}() validation failed: expected a grouped output for a group of '
260
+ f'{group_size}: either an array with leading dim {group_size} or a list of '
261
+ f'{group_size} arrays, got {type(result)}.')
262
+
263
+
216
264
  def _add_mapping_connection(user_unique_name, connection_destinations, arg_names, name, node_mapping_type):
217
265
  connection_destinations = [connection_destination for connection_destination in connection_destinations
218
266
  if not isinstance(connection_destination, SamplePreprocessResponse)]
@@ -290,11 +338,7 @@ def tensorleap_integration_test():
290
338
 
291
339
  def _validate_input_args(*args, **kwargs):
292
340
  sample_id, preprocess_response = args
293
- assert type(sample_id) == preprocess_response.sample_id_type, (
294
- f"tensorleap_integration_test validation failed: "
295
- f"sample_id type ({type(sample_id).__name__}) does not match the expected "
296
- f"type ({preprocess_response.sample_id_type}) from the PreprocessResponse."
297
- )
341
+ _validate_id_or_group(sample_id, preprocess_response, 'tensorleap_integration_test')
298
342
 
299
343
  def inner(*args, **kwargs):
300
344
  if not _call_from_tl_platform:
@@ -305,7 +349,7 @@ def tensorleap_integration_test():
305
349
  expected_names = inspect.getfullargspec(integration_test_function)[0][:2]
306
350
  if len(expected_names) < 2:
307
351
  expected_names = ['idx', 'preprocess']
308
- validate_args_structure(*args, types_order=[Union[int, str], PreprocessResponse],
352
+ validate_args_structure(*args, types_order=[Union[int, str, list], PreprocessResponse],
309
353
  func_name='integration_test', expected_names=expected_names,
310
354
  **kwargs)
311
355
  sample_id, preprocess_response = (
@@ -329,6 +373,8 @@ def tensorleap_integration_test():
329
373
  _reset_model_loop_state()
330
374
  ret = integration_test_function(*args, **kwargs)
331
375
 
376
+ global _mapping_dataset_is_grouped
377
+ _mapping_dataset_is_grouped = args[1].is_grouped
332
378
  try:
333
379
  os.environ[mapping_runtime_mode_env_var_mame] = 'True'
334
380
  _reset_model_loop_state()
@@ -349,6 +395,7 @@ def tensorleap_integration_test():
349
395
  f'Integration test is only allowed to call Tensorleap decorators. '
350
396
  f'Ensure any arithmetics, external library use, Python logic is placed within Tensorleap decoders')
351
397
  finally:
398
+ _mapping_dataset_is_grouped = False
352
399
  if mapping_runtime_mode_env_var_mame in os.environ:
353
400
  del os.environ[mapping_runtime_mode_env_var_mame]
354
401
  finally:
@@ -639,10 +686,29 @@ def tensorleap_load_model(prediction_types: Optional[List[PredictionTypeHandler]
639
686
 
640
687
  class ModelPlaceholder:
641
688
 
689
+ @staticmethod
690
+ def _reject_reused_input_source(elem, seen):
691
+ # A model input slot is wired by mutating elem.node_mapping.type, so the same
692
+ # source object at two positions silently overwrites the first slot. Each input
693
+ # encoder mints its own NodeMapping, so a collision means one source fed two
694
+ # slots — e.g. indexing a single grouped input, model([imgs[0], imgs[1]]), where
695
+ # imgs[i] returns the same node (a group is the batch dim of ONE input, not many
696
+ # inputs), or the flat model([img, img]). Reject it instead of mis-wiring.
697
+ if id(elem.node_mapping) in seen:
698
+ raise Exception(
699
+ "The same input source was passed to more than one model input slot. Each "
700
+ "model input must come from a distinct input encoder. Indexing a single "
701
+ "grouped input (e.g. model([imgs[0], imgs[1]])) feeds several samples of ONE "
702
+ "input into separate slots — a group is the batch dimension of one input, "
703
+ "not multiple inputs.")
704
+ seen.add(id(elem.node_mapping))
705
+
642
706
  # keras interface
643
707
  def __call__(self, arg):
644
708
  if isinstance(arg, list):
709
+ seen: set = set()
645
710
  for i, elem in enumerate(arg):
711
+ self._reject_reused_input_source(elem, seen)
646
712
  elem.node_mapping.type = _safe_get_item(i)
647
713
  else:
648
714
  arg.node_mapping.type = NodeMappingType.Input0
@@ -656,7 +722,9 @@ def tensorleap_load_model(prediction_types: Optional[List[PredictionTypeHandler]
656
722
  "use model.run(None, inputs) to return all outputs.")
657
723
  assert isinstance(input_dict, dict), \
658
724
  f'Expected input_dict to be a dict, got {type(input_dict)} instead.'
725
+ seen: set = set()
659
726
  for i, (input_key, elem) in enumerate(input_dict.items()):
727
+ self._reject_reused_input_source(elem, seen)
660
728
  if isinstance(input_key, NodeMappingType):
661
729
  elem.node_mapping.type = input_key
662
730
  else:
@@ -1561,13 +1629,10 @@ def tensorleap_metadata(
1561
1629
  raise Exception(f'Metadata with name {name} already exists. '
1562
1630
  f'Please choose another')
1563
1631
 
1564
- def _validate_input_args(sample_id: Union[int, str], preprocess_response: PreprocessResponse):
1565
- assert type(sample_id) == preprocess_response.sample_id_type, \
1566
- (f'{user_function.__name__}() validation failed: '
1567
- f'Argument sample_id should be as the same type as defined in the preprocess response '
1568
- f'{preprocess_response.sample_id_type}. Got {type(sample_id)}.')
1632
+ def _validate_input_args(sample_id: Union[int, str, list], preprocess_response: PreprocessResponse):
1633
+ _validate_id_or_group(sample_id, preprocess_response, user_function.__name__)
1569
1634
 
1570
- def _validate_result(result):
1635
+ def _validate_single(result):
1571
1636
  supported_result_types = (type(None), int, str, bool, float, dict, np.floating,
1572
1637
  np.bool_, np.unsignedinteger, np.signedinteger, np.integer)
1573
1638
  if isinstance(result, tuple):
@@ -1585,6 +1650,20 @@ def tensorleap_metadata(
1585
1650
  (f'{user_function.__name__}() validation failed: '
1586
1651
  f'Values in the return dict should be of type {str(supported_result_types)}. Got {type(value)}.')
1587
1652
 
1653
+ def _validate_result(result, grouped=False, group_size=None):
1654
+ if not grouped:
1655
+ _validate_single(result)
1656
+ return
1657
+ # Grouped metadata: a list of `group_size` per-sample scalars/dicts.
1658
+ assert isinstance(result, list), \
1659
+ (f'{user_function.__name__}() validation failed: expected a grouped output for a group '
1660
+ f'of {group_size}: a list of {group_size} metadata values, got {type(result)}.')
1661
+ assert len(result) == group_size, \
1662
+ (f'{user_function.__name__}() validation failed: expected a grouped output for a group '
1663
+ f'of {group_size}: a list of {group_size} metadata values, got {len(result)}.')
1664
+ for value in result:
1665
+ _validate_single(value)
1666
+
1588
1667
  def inner_without_validate(sample_id, preprocess_response):
1589
1668
 
1590
1669
  global _called_from_inside_tl_decorator
@@ -1604,14 +1683,16 @@ def tensorleap_metadata(
1604
1683
  set_current('tensorleap_metadata')
1605
1684
  if os.environ.get(mapping_runtime_mode_env_var_mame):
1606
1685
  return None
1607
- validate_args_structure(*args, types_order=[Union[int, str], PreprocessResponse],
1686
+ validate_args_structure(*args, types_order=[Union[int, str, list], PreprocessResponse],
1608
1687
  func_name=user_function.__name__, expected_names=["idx", "preprocess"], **kwargs)
1609
1688
  sample_id, preprocess_response = args if len(args) != 0 else kwargs.values()
1610
1689
  _validate_input_args(sample_id, preprocess_response)
1611
1690
 
1691
+ grouped = isinstance(sample_id, list)
1692
+ group_size = len(sample_id) if grouped else None
1612
1693
  result = inner_without_validate(sample_id, preprocess_response)
1613
1694
 
1614
- _validate_result(result)
1695
+ _validate_result(result, grouped, group_size)
1615
1696
  if not _call_from_tl_platform:
1616
1697
  update_env_params_func("tensorleap_metadata", "v")
1617
1698
  return result
@@ -1628,35 +1709,51 @@ def tensorleap_custom_latent_space(name: Optional[str] = None, use_ls_for_analys
1628
1709
 
1629
1710
  def decorating_function(user_function: SectionCallableInterface):
1630
1711
  ls_name = name if name is not None else user_function.__name__
1631
- def _validate_input_args(sample_id: Union[int, str], preprocess_response: PreprocessResponse):
1632
- assert isinstance(sample_id, (int, str)), \
1633
- (f'tensorleap_custom_latent_space validation failed: '
1634
- f'Argument sample_id should be either int or str. Got {type(sample_id)}.')
1635
- assert isinstance(preprocess_response, PreprocessResponse), \
1636
- (f'tensorleap_custom_latent_space validation failed: '
1637
- f'Argument preprocess_response should be a PreprocessResponse. Got {type(preprocess_response)}.')
1638
- assert type(sample_id) == preprocess_response.sample_id_type, \
1639
- (f'tensorleap_custom_latent_space validation failed: '
1640
- f'Argument sample_id should be as the same type as defined in the preprocess response '
1641
- f'{preprocess_response.sample_id_type}. Got {type(sample_id)}.')
1642
1712
 
1643
- def _validate_result(result):
1644
- assert isinstance(result, np.ndarray), \
1713
+ def _validate_input_args(sample_id: Union[int, str, list], preprocess_response: PreprocessResponse):
1714
+ _validate_id_or_group(sample_id, preprocess_response, 'tensorleap_custom_latent_space')
1715
+
1716
+ def _validate_single(single_result):
1717
+ assert isinstance(single_result, np.ndarray), \
1645
1718
  (f'tensorleap_custom_latent_space validation failed: '
1646
- f'The return type should be a numpy array. Got {type(result)}.')
1647
- if result.ndim > 1:
1648
- flat_dim = int(np.prod(result.shape))
1719
+ f'The return type should be a numpy array. Got {type(single_result)}.')
1720
+ if single_result.ndim > 1:
1721
+ flat_dim = int(np.prod(single_result.shape))
1649
1722
  store_general_warning(
1650
- key=("tensorleap_custom_latent_space_flatten", ls_name, tuple(result.shape)),
1723
+ key=("tensorleap_custom_latent_space_flatten", ls_name, tuple(single_result.shape)),
1651
1724
  message=(
1652
- f"tensorleap_custom_latent_space '{ls_name}' returned per-sample shape {tuple(result.shape)} "
1653
- f"(ndim={result.ndim}). Tensorleap assumes per-sample shape (d, ...) and will "
1725
+ f"tensorleap_custom_latent_space '{ls_name}' returned per-sample shape {tuple(single_result.shape)} "
1726
+ f"(ndim={single_result.ndim}). Tensorleap assumes per-sample shape (d, ...) and will "
1654
1727
  f"flatten to ({flat_dim},) before downstream visualization and clustering. "
1655
1728
  f"If you want a different aggregation (e.g. global average pooling), do it "
1656
1729
  f"inside your function."
1657
1730
  ),
1658
1731
  )
1659
1732
 
1733
+ def _validate_result(result, grouped=False, group_size=None):
1734
+ if not grouped:
1735
+ _validate_single(result)
1736
+ return
1737
+ # Grouped: (B, *) array or list of B per-sample arrays. Validate each per-sample slice so
1738
+ # the per-sample flatten check/warning uses the sample shape, not the (B, *) batch shape.
1739
+ if isinstance(result, np.ndarray):
1740
+ assert result.ndim >= 1 and result.shape[0] == group_size, \
1741
+ (f'tensorleap_custom_latent_space validation failed: expected a grouped output for a '
1742
+ f'group of {group_size}: an array with leading dim {group_size}, got shape {result.shape}.')
1743
+ for single in result:
1744
+ _validate_single(single)
1745
+ elif isinstance(result, list):
1746
+ assert len(result) == group_size, \
1747
+ (f'tensorleap_custom_latent_space validation failed: expected a grouped output for a '
1748
+ f'group of {group_size}: a list of {group_size} arrays, got {len(result)}.')
1749
+ for single in result:
1750
+ _validate_single(single)
1751
+ else:
1752
+ raise AssertionError(
1753
+ f'tensorleap_custom_latent_space validation failed: expected a grouped output for a '
1754
+ f'group of {group_size}: either an array with leading dim {group_size} or a list of '
1755
+ f'{group_size} arrays, got {type(result)}.')
1756
+
1660
1757
  def inner_without_validate(sample_id, preprocess_response):
1661
1758
  global _called_from_inside_tl_decorator
1662
1759
  _called_from_inside_tl_decorator += 1
@@ -1677,9 +1774,11 @@ def tensorleap_custom_latent_space(name: Optional[str] = None, use_ls_for_analys
1677
1774
 
1678
1775
  _validate_input_args(sample_id, preprocess_response)
1679
1776
 
1777
+ grouped = isinstance(sample_id, list)
1778
+ group_size = len(sample_id) if grouped else None
1680
1779
  result = inner_without_validate(sample_id, preprocess_response)
1681
1780
 
1682
- _validate_result(result)
1781
+ _validate_result(result, grouped, group_size)
1683
1782
  return result
1684
1783
 
1685
1784
  return inner
@@ -2648,6 +2747,15 @@ def tensorleap_preprocess():
2648
2747
  assert len(set(result)) == len(result), \
2649
2748
  (f'{user_function.__name__}() validation failed: '
2650
2749
  f'The return list should not contain duplicate PreprocessResponse objects.')
2750
+ # All-or-none: a dataset is either all grouped (nested sample_ids) or all flat.
2751
+ if len({response.is_grouped for response in result}) != 1:
2752
+ shapes = ', '.join(
2753
+ f"#{i}={'grouped' if response.is_grouped else 'flat'}"
2754
+ for i, response in enumerate(result))
2755
+ raise AssertionError(
2756
+ f'{user_function.__name__}() validation failed: '
2757
+ f'All PreprocessResponses must be either all grouped (nested sample_ids) '
2758
+ f'or all flat, but got a mix: {shapes}.')
2651
2759
 
2652
2760
  def inner(*args, **kwargs):
2653
2761
  if not _call_from_tl_platform:
@@ -2765,6 +2873,13 @@ def tensorleap_element_instance_preprocess(
2765
2873
  result = user_function()
2766
2874
  found_instance_metadata = False
2767
2875
  for preprocess_response in result:
2876
+ if preprocess_response.is_grouped:
2877
+ raise Exception(
2878
+ "tensorleap_element_instance_preprocess validation failed: a grouped "
2879
+ "(nested sample_ids) PreprocessResponse cannot also declare element "
2880
+ "instances -- instance ids are derived per scalar sample id, and the "
2881
+ "engine's grouped/batch fetch path has no per-instance id to mask by. "
2882
+ "Remove the instance encoders or don't group this dataset.")
2768
2883
  sample_ids_to_instance_mappings = {}
2769
2884
  instance_to_sample_ids_mappings = {}
2770
2885
  all_sample_ids = preprocess_response.sample_ids.copy()
@@ -2985,14 +3100,10 @@ def tensorleap_input_encoder(name: str, channel_dim=_UNSET, model_input_index=No
2985
3100
  f"channel axis in the batched tensor (batch = axis 0), so 0 is invalid. Use 1 "
2986
3101
  f"for channel-first (NCHW) and -1 for channel-last (NHWC).")
2987
3102
 
2988
- def _validate_input_args(sample_id: Union[int, str], preprocess_response: PreprocessResponse):
2989
- assert type(sample_id) == preprocess_response.sample_id_type, \
2990
- (f'{user_function.__name__}() validation failed: '
2991
- f'Argument sample_id should be as the same type as defined in the preprocess response '
2992
- f'{preprocess_response.sample_id_type}. Got {type(sample_id)}.')
3103
+ def _validate_input_args(sample_id: Union[int, str, list], preprocess_response: PreprocessResponse):
3104
+ _validate_id_or_group(sample_id, preprocess_response, user_function.__name__)
2993
3105
 
2994
- def _validate_result(result):
2995
- validate_output_structure(result, func_name=user_function.__name__, expected_type_name="np.ndarray")
3106
+ def _validate_single(result):
2996
3107
  assert isinstance(result, np.ndarray), \
2997
3108
  (f'{user_function.__name__}() validation failed: '
2998
3109
  f'Unsupported return type. Should be a numpy array. Got {type(result)}.')
@@ -3002,6 +3113,13 @@ def tensorleap_input_encoder(name: str, channel_dim=_UNSET, model_input_index=No
3002
3113
  assert channel_dim - 1 <= len(result.shape), (f'{user_function.__name__}() validation failed: '
3003
3114
  f'The channel_dim ({channel_dim}) should be <= to the rank of the resulting input rank ({len(result.shape)}).')
3004
3115
 
3116
+ def _validate_result(result, grouped=False, group_size=None):
3117
+ if not grouped:
3118
+ validate_output_structure(result, func_name=user_function.__name__, expected_type_name="np.ndarray")
3119
+ _validate_single(result)
3120
+ return
3121
+ _validate_grouped_result(result, group_size, user_function.__name__, _validate_single)
3122
+
3005
3123
  def inner_without_validate(sample_id, preprocess_response):
3006
3124
  global _called_from_inside_tl_decorator
3007
3125
  _called_from_inside_tl_decorator += 1
@@ -3018,18 +3136,29 @@ def tensorleap_input_encoder(name: str, channel_dim=_UNSET, model_input_index=No
3018
3136
  def inner(*args, **kwargs):
3019
3137
  if not _call_from_tl_platform:
3020
3138
  set_current("tensorleap_input_encoder")
3021
- validate_args_structure(*args, types_order=[Union[int, str], PreprocessResponse],
3139
+ validate_args_structure(*args, types_order=[Union[int, str, list], PreprocessResponse],
3022
3140
  func_name=user_function.__name__, expected_names=["idx", "preprocess"], **kwargs)
3023
3141
  sample_id, preprocess_response = args if len(args) != 0 else kwargs.values()
3024
3142
  _validate_input_args(sample_id, preprocess_response)
3025
3143
 
3144
+ grouped = isinstance(sample_id, list)
3145
+ group_size = len(sample_id) if grouped else None
3026
3146
  result = inner_without_validate(sample_id, preprocess_response)
3027
3147
 
3028
- _validate_result(result)
3148
+ _validate_result(result, grouped, group_size)
3029
3149
 
3030
3150
  if _called_from_inside_tl_decorator == 0 and _called_from_inside_tl_integration_test_decorator:
3031
- batch_warning(result, user_function.__name__)
3032
- result = np.expand_dims(result, axis=0)
3151
+ if grouped:
3152
+ # A grouped result is a list of per-sample arrays (never stacked, so a
3153
+ # B=1-only model can be fed one sample at a time). A user may also return a
3154
+ # supported (B, *) array; split it back into per-sample rows first (mirrors
3155
+ # LeapLoader._to_grouped_list) so both forms match the runtime path. Then add
3156
+ # a batch dim to each sample, mirroring the flat path's single-sample expand_dims.
3157
+ rows = result if isinstance(result, list) else [row for row in np.asarray(result)]
3158
+ result = [np.expand_dims(r, axis=0) for r in rows]
3159
+ else:
3160
+ batch_warning(result, user_function.__name__)
3161
+ result = np.expand_dims(result, axis=0)
3033
3162
  # Emit integration test event once per test
3034
3163
  try:
3035
3164
  emit_integration_event_once(AnalyticsEvent.INPUT_ENCODER_INTEGRATION_TEST, {
@@ -3050,8 +3179,15 @@ def tensorleap_input_encoder(name: str, channel_dim=_UNSET, model_input_index=No
3050
3179
  inner.node_mapping = NodeMapping(name, node_mapping_type)
3051
3180
 
3052
3181
  def mapping_inner(*args, **kwargs):
3053
- class TempMapping:
3054
- pass
3182
+ if _mapping_dataset_is_grouped:
3183
+ class TempMapping:
3184
+ def __getitem__(self, key):
3185
+ # Indexing a grouped input group (input_list[i]) selects a sample from the
3186
+ # same input source; the model wiring is identical to the un-grouped input.
3187
+ return self
3188
+ else:
3189
+ class TempMapping:
3190
+ pass
3055
3191
 
3056
3192
  ret = TempMapping()
3057
3193
  ret.node_mapping = mapping_inner.node_mapping
@@ -3081,15 +3217,10 @@ def tensorleap_gt_encoder(name: str):
3081
3217
  raise Exception(f'GT with name {name} already exists. '
3082
3218
  f'Please choose another')
3083
3219
 
3084
- def _validate_input_args(sample_id: Union[int, str], preprocess_response: PreprocessResponse):
3085
- assert type(sample_id) == preprocess_response.sample_id_type, \
3086
- (f'{user_function.__name__}() validation failed: '
3087
- f'Argument sample_id should be as the same type as defined in the preprocess response '
3088
- f'{preprocess_response.sample_id_type}. Got {type(sample_id)}.')
3220
+ def _validate_input_args(sample_id: Union[int, str, list], preprocess_response: PreprocessResponse):
3221
+ _validate_id_or_group(sample_id, preprocess_response, user_function.__name__)
3089
3222
 
3090
- def _validate_result(result):
3091
- validate_output_structure(result, func_name=user_function.__name__, expected_type_name="np.ndarray",
3092
- gt_flag=True)
3223
+ def _validate_single(result):
3093
3224
  assert isinstance(result, np.ndarray), \
3094
3225
  (f'{user_function.__name__}() validation failed: '
3095
3226
  f'Unsupported return type. Should be a numpy array. Got {type(result)}.')
@@ -3097,6 +3228,14 @@ def tensorleap_gt_encoder(name: str):
3097
3228
  (f'{user_function.__name__}() validation failed: '
3098
3229
  f'The return type should be a numpy array of type float32. Got {result.dtype}.')
3099
3230
 
3231
+ def _validate_result(result, grouped=False, group_size=None):
3232
+ if not grouped:
3233
+ validate_output_structure(result, func_name=user_function.__name__, expected_type_name="np.ndarray",
3234
+ gt_flag=True)
3235
+ _validate_single(result)
3236
+ return
3237
+ _validate_grouped_result(result, group_size, user_function.__name__, _validate_single)
3238
+
3100
3239
  def inner_without_validate(sample_id, preprocess_response):
3101
3240
  global _called_from_inside_tl_decorator
3102
3241
  _called_from_inside_tl_decorator += 1
@@ -3113,18 +3252,29 @@ def tensorleap_gt_encoder(name: str):
3113
3252
  def inner(*args, **kwargs):
3114
3253
  if not _call_from_tl_platform:
3115
3254
  set_current("tensorleap_gt_encoder")
3116
- validate_args_structure(*args, types_order=[Union[int, str], PreprocessResponse],
3255
+ validate_args_structure(*args, types_order=[Union[int, str, list], PreprocessResponse],
3117
3256
  func_name=user_function.__name__, expected_names=["idx", "preprocess"], **kwargs)
3118
- sample_id, preprocess_response = args
3257
+ sample_id, preprocess_response = args if len(args) != 0 else kwargs.values()
3119
3258
  _validate_input_args(sample_id, preprocess_response)
3120
3259
 
3260
+ grouped = isinstance(sample_id, list)
3261
+ group_size = len(sample_id) if grouped else None
3121
3262
  result = inner_without_validate(sample_id, preprocess_response)
3122
3263
 
3123
- _validate_result(result)
3264
+ _validate_result(result, grouped, group_size)
3124
3265
 
3125
3266
  if _called_from_inside_tl_decorator == 0 and _called_from_inside_tl_integration_test_decorator:
3126
- batch_warning(result, user_function.__name__)
3127
- result = np.expand_dims(result, axis=0)
3267
+ if grouped:
3268
+ # A grouped result is a list of per-sample arrays (never stacked, so a
3269
+ # B=1-only model can be fed one sample at a time). A user may also return a
3270
+ # supported (B, *) array; split it back into per-sample rows first (mirrors
3271
+ # LeapLoader._to_grouped_list) so both forms match the runtime path. Then add
3272
+ # a batch dim to each sample, mirroring the flat path's single-sample expand_dims.
3273
+ rows = result if isinstance(result, list) else [row for row in np.asarray(result)]
3274
+ result = [np.expand_dims(r, axis=0) for r in rows]
3275
+ else:
3276
+ batch_warning(result, user_function.__name__)
3277
+ result = np.expand_dims(result, axis=0)
3128
3278
  _register_chain_artifact(result, 'ground_truth')
3129
3279
  # Emit integration test event once per test
3130
3280
  try:
@@ -3140,8 +3290,15 @@ def tensorleap_gt_encoder(name: str):
3140
3290
  inner.node_mapping = NodeMapping(name, NodeMappingType.GroundTruth)
3141
3291
 
3142
3292
  def mapping_inner(*args, **kwargs):
3143
- class TempMapping:
3144
- pass
3293
+ if _mapping_dataset_is_grouped:
3294
+ class TempMapping:
3295
+ def __getitem__(self, key):
3296
+ # Indexing a grouped GT group (gt_list[i]) selects a sample from the same
3297
+ # ground-truth source; wiring is identical to the un-grouped GT node.
3298
+ return self
3299
+ else:
3300
+ class TempMapping:
3301
+ pass
3145
3302
 
3146
3303
  ret = TempMapping()
3147
3304
  ret.node_mapping = mapping_inner.node_mapping
code_loader/leaploader.py CHANGED
@@ -41,6 +41,11 @@ def _serialize_sim_bounds(bounds) -> dict:
41
41
 
42
42
 
43
43
  class LeapLoader(LeapLoaderBase):
44
+ # Opt-in ceiling (env var) on samples materialized per get_samples call — the engine sets it to
45
+ # turn a mistaken whole-group ask into a clear error instead of a silent worker OOM. Unset => no
46
+ # ceiling. See get_samples and grouped-fetch-oom-bug.md.
47
+ _grouped_fetch_max_samples_env = "LEAP_GROUPED_FETCH_MAX_SAMPLES_PER_CALL"
48
+
44
49
  def __init__(self, code_path: str, code_entry_name: str):
45
50
  super().__init__(code_path, code_entry_name)
46
51
 
@@ -60,14 +65,36 @@ class LeapLoader(LeapLoaderBase):
60
65
 
61
66
  @lru_cache()
62
67
  def exec_script(self) -> None:
68
+ from code_loader.inner_leap_binder import leapbinder_decorators as _leap_dec
63
69
  try:
64
70
  os.environ[mapping_runtime_mode_env_var_mame] = 'TRUE'
65
71
  self.evaluate_module()
72
+
73
+ # A grouped integration test may index a grouped input/gt (image_list[i]) to wire a
74
+ # single sample of a group; that indexing is only valid when the mapping graph is
75
+ # built in grouped mode. Detect grouped-ness so the placeholder pass below matches
76
+ # the dataset. Preprocess is stubbed while mapping mode is on, so run it with mapping
77
+ # mode momentarily disabled, and cache it so _preprocess_result() doesn't re-run it.
78
+ is_grouped = False
79
+ if global_leap_binder.integration_test_func is not None:
80
+ del os.environ[mapping_runtime_mode_env_var_mame]
81
+ try:
82
+ preprocess_result = global_leap_binder.get_preprocess_result()
83
+ self._preprocess_result_cached = preprocess_result
84
+ is_grouped = any(r.is_grouped for r in preprocess_result.values())
85
+ finally:
86
+ os.environ[mapping_runtime_mode_env_var_mame] = 'TRUE'
87
+
88
+ _leap_dec._mapping_dataset_is_grouped = is_grouped
66
89
  if global_leap_binder.integration_test_func is not None:
67
90
  from code_loader.inner_leap_binder.leapbinder_decorators import \
68
91
  _reset_model_loop_state
69
92
  _reset_model_loop_state()
70
- global_leap_binder.integration_test_func(None, PreprocessResponse(state=DataStateType.training, length=0))
93
+ mapping_preprocess = (
94
+ PreprocessResponse(sample_ids=[["__mapping_placeholder__"]], state=DataStateType.training)
95
+ if is_grouped else
96
+ PreprocessResponse(state=DataStateType.training, length=0))
97
+ global_leap_binder.integration_test_func(None, mapping_preprocess)
71
98
  except TypeError as e:
72
99
  import traceback
73
100
  global_leap_binder.setup_container = DatasetIntegrationSetup()
@@ -80,6 +107,7 @@ class LeapLoader(LeapLoaderBase):
80
107
  raise DatasetScriptException(getattr(e, 'message', repr(e))) from e
81
108
  finally:
82
109
  # ensure that the environment variable is removed after the script execution
110
+ _leap_dec._mapping_dataset_is_grouped = False
83
111
  if mapping_runtime_mode_env_var_mame in os.environ:
84
112
  del os.environ[mapping_runtime_mode_env_var_mame]
85
113
 
@@ -209,7 +237,7 @@ class LeapLoader(LeapLoaderBase):
209
237
  additional = self._preprocess_result().get(DataStateEnum.additional)
210
238
  if additional is None:
211
239
  return set()
212
- return set(additional.sample_ids)
240
+ return set(additional.flat_sample_ids)
213
241
 
214
242
  def _resolve_synthetic(self, sample_id: Union[int, str],
215
243
  state: Optional[DataStateEnum] = None
@@ -250,7 +278,7 @@ class LeapLoader(LeapLoaderBase):
250
278
  )
251
279
 
252
280
  preprocess_result = self._preprocess_result()
253
- if state == DataStateEnum.unlabeled and sample_id not in preprocess_result[state].sample_ids:
281
+ if state == DataStateEnum.unlabeled and sample_id not in preprocess_result[state].flat_sample_ids:
254
282
  self._preprocess_result(update_unlabeled_preprocess=True)
255
283
 
256
284
  metadata, metadata_is_none = self.get_metadata(state, sample_id)
@@ -379,10 +407,10 @@ class LeapLoader(LeapLoaderBase):
379
407
  test_result = DatasetTestResultPayload('preprocess')
380
408
  try:
381
409
  preprocess_result = self._preprocess_result()
382
- if self.get_sample_id_type() is str:
410
+ if issubclass(self.get_sample_id_type(), str):
383
411
  max_allowed_item_size = np.dtype('<U256').itemsize
384
412
  for state, preprocess_response in preprocess_result.items():
385
- sample_ids_array = np.array(preprocess_response.sample_ids)
413
+ sample_ids_array = np.array(preprocess_response.flat_sample_ids)
386
414
  if sample_ids_array.dtype.itemsize > max_allowed_item_size:
387
415
  raise Exception(f"Sample id are too long. Max allowed length is 256 charecters.")
388
416
 
@@ -443,6 +471,10 @@ class LeapLoader(LeapLoaderBase):
443
471
  preprocess_response.tl_generated = True
444
472
  if preprocess_response.length < 1:
445
473
  raise ValueError("Simulation returned PreprocessResponse with length < 1")
474
+ if preprocess_response.is_grouped:
475
+ raise ValueError(
476
+ "Simulation returned a grouped PreprocessResponse; simulations must "
477
+ "return a flat (non-grouped) PreprocessResponse.")
446
478
  preprocess_response.sample_ids = [0]
447
479
  for handler in global_leap_binder.setup_container.inputs:
448
480
  out1 = handler.function(preprocess_response.sample_ids[0], preprocess_response)
@@ -478,7 +510,10 @@ class LeapLoader(LeapLoaderBase):
478
510
  if preprocess_response.sample_ids_to_instance_mappings:
479
511
  raise Exception('Element instances are not supported together with '
480
512
  'tensorleap_autoregressive_step.')
481
- sample_id = preprocess_response.sample_ids[0]
513
+ # Grouped preprocess is fine: grouping only amortizes the shared base reads
514
+ # (GT/metadata/custom_latent) — the AR hook always drives a single scalar chain
515
+ # and is never handed a group.
516
+ sample_id = preprocess_response.flat_sample_ids[0]
482
517
  first_result = handler.function(sample_id, None, None, None, preprocess_response)
483
518
  second_result = handler.function(sample_id, None, None, None, preprocess_response)
484
519
  if not isinstance(first_result, tuple) or len(first_result) != 2:
@@ -559,7 +594,7 @@ class LeapLoader(LeapLoaderBase):
559
594
  if handler.input_shapes is None:
560
595
  preprocess_result = self._preprocess_result()
561
596
  first_state_response = next(iter(preprocess_result.values()))
562
- first_sample_id = first_state_response.sample_ids[0]
597
+ first_sample_id = first_state_response.flat_sample_ids[0]
563
598
  first_result = handler.function(first_sample_id, None, None, None,
564
599
  first_state_response)
565
600
  if not isinstance(first_result, tuple) or len(first_result) != 2 or \
@@ -898,6 +933,57 @@ class LeapLoader(LeapLoaderBase):
898
933
 
899
934
  return sample_ids
900
935
 
936
+ @staticmethod
937
+ def _grouped_dict_keys(rows: List[Dict[str, Any]], handler_name: str) -> List[str]:
938
+ """Keys for a group of dict-metadata rows, requiring every row to share the same keys.
939
+ A group must expose a uniform metadata schema; otherwise the per-key lists would silently
940
+ drop or KeyError on rows that differ from row 0."""
941
+ keys = list(rows[0].keys())
942
+ key_set = set(keys)
943
+ for pos, row in enumerate(rows):
944
+ if set(row.keys()) != key_set:
945
+ raise ValueError(
946
+ f"Metadata handler {handler_name!r} returned inconsistent dict keys across the "
947
+ f"group: row 0 has {sorted(key_set)} but row {pos} has {sorted(row.keys())}. "
948
+ f"All rows in a group must share the same metadata keys.")
949
+ return keys
950
+
951
+ @staticmethod
952
+ def _to_grouped_list(raw: Any) -> List[npt.NDArray[np.float32]]:
953
+ """Normalize a grouped encoder result to a list of per-sample arrays.
954
+
955
+ Grouped results are NEVER stacked into a (B, *) batch. Grouping is only a storage
956
+ read-unit (one file load per group); assembling the model batch (B) is the engine's
957
+ single responsibility, so it stays the only place samples get batched. Keeping
958
+ per-sample arrays lets the engine batch to any B — including a B=1-only model — without
959
+ an intermediate stack/unstack. A user encoder that already returned a stacked (B, *)
960
+ array is split back into its per-sample rows so the contract is a list either way."""
961
+ if isinstance(raw, list):
962
+ return raw
963
+ return [row for row in np.asarray(raw)]
964
+
965
+ def _locate_group(self, preprocess_state: PreprocessResponse,
966
+ sample_id: Union[int, str]) -> Tuple[List[Union[int, str]], int]:
967
+ """Map a sample id to (its group, its position in that group). Cached on the
968
+ PreprocessResponse itself (not keyed by id() on this loader) so grouped single-sample
969
+ fetches don't rescan the groups, and the cache can never go stale: when the response is
970
+ replaced (e.g. unlabeled preprocess refresh), the old object and its cache are simply
971
+ discarded together instead of lingering under a possibly-reused id()."""
972
+ mapping = preprocess_state._group_pos_cache
973
+ if mapping is None:
974
+ mapping = {}
975
+ for group in preprocess_state.groups:
976
+ for pos, sid in enumerate(group):
977
+ if sid in mapping:
978
+ raise ValueError(
979
+ f"Duplicate sample id {sid!r} across groups in the preprocess response; "
980
+ f"sample ids must be unique within and across groups.")
981
+ mapping[sid] = (group, pos)
982
+ preprocess_state._group_pos_cache = mapping
983
+ if sample_id not in mapping:
984
+ raise KeyError(f"Sample id {sample_id!r} is not present in any group of this preprocess response.")
985
+ return mapping[sample_id]
986
+
901
987
  def _get_dataset_handlers(self, handlers: Iterable[DatasetBaseHandler],
902
988
  state: DataStateEnum, sample_id: Union[int, str]) -> Dict[str, npt.NDArray[np.float32]]:
903
989
  result_agg = {}
@@ -909,12 +995,127 @@ class LeapLoader(LeapLoaderBase):
909
995
  sample_id = original_local_id
910
996
  else:
911
997
  preprocess_state = preprocess_result[state]
998
+ if preprocess_state.is_grouped:
999
+ # Grouped dataset, SINGLE-sample fetch: encode only THIS sample, never the whole group.
1000
+ # Grouping is an I/O read-unit, not an encode-unit — the integration's per-file cache
1001
+ # makes the group's file decode once across consecutive per-sample calls, so a singleton
1002
+ # ([sample_id]) is both a valid single-group subset of the get_samples contract and
1003
+ # correct (samples in a group are independent; see _to_grouped_list). Handing the encoder
1004
+ # the whole group here just to index one sample out materialized B encoded tensors per
1005
+ # sample — a driver of the grouped-fetch worker OOM (see grouped-fetch-oom-bug.md).
1006
+ self._locate_group(preprocess_state, sample_id) # validates the id belongs to a group
1007
+ for handler in handlers:
1008
+ result_agg[handler.name] = self._to_grouped_list(
1009
+ handler.function([sample_id], preprocess_state))[0]
1010
+ return result_agg
912
1011
  for handler in handlers:
913
1012
  handler_result = handler.function(sample_id, preprocess_state)
914
1013
  handler_name = handler.name
915
1014
  result_agg[handler_name] = handler_result
916
1015
  return result_agg
917
1016
 
1017
+ def get_samples(self, state: DataStateEnum, group_ids: List[Union[int, str]]) -> DatasetSample:
1018
+ """Group-aware fetch: hand the whole group to each encoder in a single call and return a
1019
+ grouped DatasetSample (inputs/gt as per-sample lists of length B, metadata as per-key lists
1020
+ of length B, index = the group). Grouped results are never stacked here — batching the
1021
+ samples into the model's B is the engine's job. This is the path a group-aware engine calls
1022
+ to read a file once per group. Row order matches group_ids exactly."""
1023
+ self.exec_script()
1024
+ preprocess_result = self._preprocess_result()
1025
+ if state == DataStateEnum.unlabeled and any(
1026
+ sid not in preprocess_result[state].flat_sample_ids for sid in group_ids):
1027
+ # Mirrors get_sample's refresh: the unlabeled preprocess can grow between calls, so a
1028
+ # group_id absent from the current snapshot may just not have been generated yet.
1029
+ self._preprocess_result(update_unlabeled_preprocess=True)
1030
+ preprocess_state = preprocess_result[state]
1031
+ assert preprocess_state.is_grouped, (
1032
+ "get_samples is the group-aware fetch path and requires a grouped preprocess "
1033
+ "response; call get_sample for a flat (non-grouped) dataset.")
1034
+ # All requested ids must belong to a single group (one file). A cross-group request
1035
+ # would force the encoder to load multiple files, defeating the one-load-per-group
1036
+ # contract; partitioning a scattered request by group is the engine's responsibility.
1037
+ distinct_groups = {id(self._locate_group(preprocess_state, sid)[0]) for sid in group_ids}
1038
+ assert len(distinct_groups) == 1, (
1039
+ f"get_samples received sample ids spanning {len(distinct_groups)} groups; a request "
1040
+ f"must be confined to a single group. The engine partitions cross-group requests and "
1041
+ f"issues one get_samples call per group.")
1042
+ # Opt-in safety net for the grouped-fetch OOM (grouped-fetch-oom-bug.md). This call
1043
+ # materializes len(group_ids) encoded samples at once; on a large group/file that peak is
1044
+ # what kernel-OOM-kills the worker. A group's file is decoded once by the integration's
1045
+ # per-file cache regardless of how the encode is chunked, so the engine should request
1046
+ # memory-bounded sub-chunks of a group (each a valid single-group subset, e.g.
1047
+ # get_samples(state, group_ids[i:j])) rather than the whole group. When the engine sets the
1048
+ # ceiling below, an over-large ask fails here with a message that names the lever instead of
1049
+ # a silent GENERIC_MEMORY_LIMIT kill. Unset => no ceiling (preserves current whole-group callers).
1050
+ max_per_call = os.environ.get(self._grouped_fetch_max_samples_env)
1051
+ if max_per_call is not None and len(group_ids) > int(max_per_call):
1052
+ raise ValueError(
1053
+ f"get_samples asked to materialize {len(group_ids)} samples in a single call, above "
1054
+ f"the configured ceiling {max_per_call} ({self._grouped_fetch_max_samples_env}). The "
1055
+ f"group's file is decoded once via the integration's per-file cache regardless of "
1056
+ f"chunking, so request memory-bounded sub-chunks of the group "
1057
+ f"(get_samples(state, group_ids[i:j])) instead of the whole group. "
1058
+ f"See grouped-fetch-oom-bug.md.")
1059
+ inputs = {handler.name: self._to_grouped_list(handler.function(group_ids, preprocess_state))
1060
+ for handler in global_leap_binder.setup_container.inputs}
1061
+ autoregressive_handler = global_leap_binder.setup_container.autoregressive_step
1062
+ if autoregressive_handler is not None:
1063
+ # AR integrations have no input encoders — the hook's first call supplies each
1064
+ # chain's step-0 model inputs. The hook is strictly per-scalar-chain (never handed
1065
+ # a group), so it is called once per flat id in the group and the per-sample results
1066
+ # are collected into per-key lists of length B, matching the grouped (never-stacked)
1067
+ # contract used for every other encoder here.
1068
+ step_zero_per_sample = []
1069
+ for sid in group_ids:
1070
+ step_zero_result = autoregressive_handler.function(sid, None, None, None,
1071
+ preprocess_state)
1072
+ if not isinstance(step_zero_result, tuple) or len(step_zero_result) != 2 or \
1073
+ not isinstance(step_zero_result[0], dict):
1074
+ raise Exception(
1075
+ 'The autoregressive step hook must return a (next_inputs, state) tuple '
1076
+ f'with a dict of initial model inputs on its first call, got '
1077
+ f'{type(step_zero_result)} for sample {sid}.')
1078
+ self._validate_autoregressive_step_inputs(step_zero_result[0], sid)
1079
+ step_zero_per_sample.append(step_zero_result[0])
1080
+ if step_zero_per_sample:
1081
+ for key in self._grouped_dict_keys(step_zero_per_sample, autoregressive_handler.name):
1082
+ inputs[key] = [row[key] for row in step_zero_per_sample]
1083
+ gt = None
1084
+ if state != DataStateEnum.unlabeled:
1085
+ gt = {handler.name: self._to_grouped_list(handler.function(group_ids, preprocess_state))
1086
+ for handler in global_leap_binder.setup_container.ground_truths}
1087
+
1088
+ metadata: Dict[str, Any] = {}
1089
+ metadata_is_none: Dict[str, Any] = {}
1090
+ for handler in global_leap_binder.setup_container.metadata:
1091
+ rows = handler.function(group_ids, preprocess_state) # length-B sequence of scalars/dicts
1092
+ if len(rows) > 0 and isinstance(rows[0], dict):
1093
+ for key in self._grouped_dict_keys(rows, handler.name):
1094
+ name = "{}_{}".format(handler.name, key)
1095
+ converted = [self._convert_metadata_to_correct_type(name, row[key]) for row in rows]
1096
+ metadata[name] = [v for v, _ in converted]
1097
+ metadata_is_none[name] = [is_none for _, is_none in converted]
1098
+ else:
1099
+ converted = [self._convert_metadata_to_correct_type(handler.name, row) for row in rows]
1100
+ metadata[handler.name] = [v for v, _ in converted]
1101
+ metadata_is_none[handler.name] = [is_none for _, is_none in converted]
1102
+
1103
+ # custom_latent_spaces are group-aware like the input/GT encoders: hand each the whole group
1104
+ # in a single call so a file-backed latent fn loads the file once, and normalize to per-sample
1105
+ # lists. This keeps the mandatory group path non-lossy. instance_masks stay None here: they are
1106
+ # a per-(sample, instance) concern that needs an instance_id the group fetch does not carry.
1107
+ latent_handlers = global_leap_binder.setup_container.custom_latent_spaces
1108
+ custom_latent_spaces = None
1109
+ if latent_handlers:
1110
+ custom_latent_spaces = {
1111
+ name: self._to_grouped_list(handler.function(group_ids, preprocess_state))
1112
+ for name, handler in latent_handlers.items()
1113
+ }
1114
+
1115
+ return DatasetSample(inputs=inputs, gt=gt, metadata=metadata, metadata_is_none=metadata_is_none,
1116
+ index=list(group_ids), state=state, custom_latent_spaces=custom_latent_spaces,
1117
+ instance_masks=None)
1118
+
918
1119
  def _get_inputs(self, state: DataStateEnum, sample_id: Union[int, str]) -> Dict[str, npt.NDArray[np.float32]]:
919
1120
  inputs = self._get_dataset_handlers(global_leap_binder.setup_container.inputs, state, sample_id)
920
1121
  autoregressive_handler = global_leap_binder.setup_container.autoregressive_step
@@ -1018,12 +1219,25 @@ class LeapLoader(LeapLoaderBase):
1018
1219
  sample_id = original_local_id
1019
1220
  else:
1020
1221
  preprocess_state = preprocess_result[state]
1222
+ if preprocess_state.is_grouped:
1223
+ self._locate_group(preprocess_state, sample_id) # validates the id belongs to a group
1021
1224
  for handler in global_leap_binder.setup_container.metadata:
1022
1225
  if requested_metadata_names:
1023
1226
  if not is_metadata_name_starts_with_handler_name(handler):
1024
1227
  continue
1025
1228
 
1026
- handler_result = handler.function(sample_id, preprocess_state)
1229
+ if preprocess_state.is_grouped:
1230
+ # Single-sample metadata fetch: encode only THIS sample, never the whole group —
1231
+ # same memory rationale as _get_dataset_handlers / the get_sample latent branch
1232
+ # (grouped-fetch-oom-bug.md). Metadata values are not guaranteed small scalars: a
1233
+ # metadata encoder may return an np.asarray or read the sample's heavy data to
1234
+ # compute a statistic, so materializing all B rows just to index one out is the same
1235
+ # OOM path this fix closes. The batched get_metadata_multiple_samples correctly keeps
1236
+ # the whole-group encode (amortized across many ids via its group_cache); a single
1237
+ # id does not need it. A singleton is a valid single-group subset of the contract.
1238
+ handler_result = handler.function([sample_id], preprocess_state)[0]
1239
+ else:
1240
+ handler_result = handler.function(sample_id, preprocess_state)
1027
1241
  if isinstance(handler_result, dict):
1028
1242
  for single_metadata_name, single_metadata_result in handler_result.items():
1029
1243
  handler_name = f'{handler.name}_{single_metadata_name}'
@@ -1042,6 +1256,66 @@ class LeapLoader(LeapLoaderBase):
1042
1256
 
1043
1257
  return result_agg, is_none
1044
1258
 
1259
+ def _grouped_metadata_for_group(self, preprocess_state: PreprocessResponse,
1260
+ group_ids: List[Union[int, str]],
1261
+ requested_metadata_names: Optional[List[str]]
1262
+ ) -> Tuple[Dict[str, List[Any]], Dict[str, List[bool]]]:
1263
+ def is_wanted(_handler):
1264
+ if not requested_metadata_names:
1265
+ return True
1266
+ for metadata_name in requested_metadata_names:
1267
+ if metadata_name.startswith(_handler.name + '_') or metadata_name == _handler.name:
1268
+ return True
1269
+ return False
1270
+
1271
+ results: Dict[str, List[Any]] = {}
1272
+ is_none: Dict[str, List[bool]] = {}
1273
+ for handler in global_leap_binder.setup_container.metadata:
1274
+ if requested_metadata_names and not is_wanted(handler):
1275
+ continue
1276
+ rows = handler.function(group_ids, preprocess_state)
1277
+ if len(rows) > 0 and isinstance(rows[0], dict):
1278
+ for key in self._grouped_dict_keys(rows, handler.name):
1279
+ name = f'{handler.name}_{key}'
1280
+ if requested_metadata_names and name not in requested_metadata_names:
1281
+ continue
1282
+ converted = [self._convert_metadata_to_correct_type(name, row[key]) for row in rows]
1283
+ results[name] = [v for v, _ in converted]
1284
+ is_none[name] = [n for _, n in converted]
1285
+ else:
1286
+ name = handler.name
1287
+ if requested_metadata_names and name not in requested_metadata_names:
1288
+ continue
1289
+ converted = [self._convert_metadata_to_correct_type(name, row) for row in rows]
1290
+ results[name] = [v for v, _ in converted]
1291
+ is_none[name] = [n for _, n in converted]
1292
+ return results, is_none
1293
+
1294
+ def get_metadata_multiple_samples(self, state: DataStateEnum, sample_ids: Union[List[int], List[str]],
1295
+ requested_metadata_names: Optional[List[str]] = None
1296
+ ) -> Tuple[Dict[str, Union[List[str], List[int], List[bool], List[float]]],
1297
+ Dict[str, List[bool]]]:
1298
+ preprocess_state = self._preprocess_result().get(state)
1299
+ if preprocess_state is None or not preprocess_state.is_grouped:
1300
+ return super().get_metadata_multiple_samples(state, sample_ids, requested_metadata_names)
1301
+
1302
+ sample_id_type = self.get_sample_id_type()
1303
+ aggregated_results: Dict[str, List[Any]] = {}
1304
+ aggregated_is_none: Dict[str, List[bool]] = {}
1305
+ group_cache: Dict[int, Tuple[Dict[str, List[Any]], Dict[str, List[bool]]]] = {}
1306
+ for sample_id in sample_ids:
1307
+ sample_id = sample_id_type(sample_id)
1308
+ group_ids, pos = self._locate_group(preprocess_state, sample_id)
1309
+ key = id(group_ids)
1310
+ if key not in group_cache:
1311
+ group_cache[key] = self._grouped_metadata_for_group(
1312
+ preprocess_state, group_ids, requested_metadata_names)
1313
+ group_results, group_is_none = group_cache[key]
1314
+ for name, values in group_results.items():
1315
+ aggregated_results.setdefault(name, []).append(values[pos])
1316
+ aggregated_is_none.setdefault(name, []).append(group_is_none[name][pos])
1317
+ return aggregated_results, aggregated_is_none
1318
+
1045
1319
  @lru_cache()
1046
1320
  def get_sample_id_type(self) -> Type:
1047
1321
  preprocess_results = list(self._preprocess_result().values())
@@ -1052,13 +1326,19 @@ class LeapLoader(LeapLoaderBase):
1052
1326
 
1053
1327
  return id_type
1054
1328
 
1055
- @staticmethod
1056
1329
  def _get_custom_latent_spaces(
1330
+ self,
1057
1331
  sample_id: Union[int, str],
1058
1332
  preprocess: "PreprocessResponse") -> Optional[Dict[str, npt.NDArray[np.float32]]]:
1059
1333
  handlers = global_leap_binder.setup_container.custom_latent_spaces
1060
1334
  if not handlers:
1061
1335
  return None
1336
+ if preprocess.is_grouped:
1337
+ # Single-sample fetch: encode this sample only, not the whole group (same
1338
+ # memory rationale as _get_dataset_handlers; see grouped-fetch-oom-bug.md).
1339
+ self._locate_group(preprocess, sample_id) # validates group membership
1340
+ return {name: self._to_grouped_list(handler.function([sample_id], preprocess))[0]
1341
+ for name, handler in handlers.items()}
1062
1342
  return {name: handler.function(sample_id, preprocess) for name, handler in handlers.items()}
1063
1343
 
1064
1344
  @lru_cache()
code_loader/utils.py CHANGED
@@ -17,6 +17,16 @@ from code_loader.contract.enums import DatasetMetadataType
17
17
  def to_numpy_return_wrapper(encoder_function: SectionCallableInterface) -> SectionCallableInterface:
18
18
  def numpy_encoder_function(idx: Union[int, str], samples: PreprocessResponse) -> npt.NDArray[np.float32]:
19
19
  result = encoder_function(idx, samples)
20
+ if isinstance(idx, list):
21
+ # Grouped call (idx is a list of sample ids): never stack the per-sample results into a
22
+ # (B, *) batch — grouping is only a storage read-unit; batching the group to the model's
23
+ # B is the engine's single responsibility. Gate on the call shape (idx is a list), not
24
+ # the result shape, so a flat encoder returning a list of arrays still stacks below.
25
+ # Normalize each sample independently (a user-stacked (B, *) array is split back into its
26
+ # rows) so a ragged group — variable per-sample shapes, e.g. token lists of differing
27
+ # length — is preserved instead of crashing np.array() on an inhomogeneous shape.
28
+ rows = result if isinstance(result, list) else np.asarray(result)
29
+ return [np.asarray(sample) for sample in rows]
20
30
  numpy_result: npt.NDArray[np.float32] = np.array(result)
21
31
  return numpy_result
22
32
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: code-loader
3
- Version: 1.0.197.dev1
3
+ Version: 1.0.197.dev2
4
4
  Summary:
5
5
  Home-page: https://github.com/tensorleap/code-loader
6
6
  License: MIT
@@ -1,7 +1,7 @@
1
1
  LICENSE,sha256=qIwWjdspQeSMTtnFZBC8MuT-95L02FPvzRUdWFxrwJY,1067
2
2
  code_loader/__init__.py,sha256=outxRQ0M-zMfV0QGVJmAed5qWfRmyD0TV6-goEGAzBw,406
3
3
  code_loader/contract/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- code_loader/contract/datasetclasses.py,sha256=LjYe-Ub_WzjXaloFKxwoL1sT1sWOrFSFU6XXxEoYENs,13826
4
+ code_loader/contract/datasetclasses.py,sha256=LrhOsNirP3MRKUDQcI6u0r-GeSGHppdSUNzVEAkADeU,16816
5
5
  code_loader/contract/enums.py,sha256=2q-IV_5g9lLE306DIbWA1c0tn5IhDtxsKxyV1x_Lreg,1671
6
6
  code_loader/contract/exceptions.py,sha256=jWqu5i7t-0IG0jGRsKF4DjJdrsdpJjIYpUkN1F4RiyQ,51
7
7
  code_loader/contract/mapping.py,sha256=sWJhpng-IkOzQnWQdMT5w2ZZ3X1Z_OOzSwCLXIS7oxE,1446
@@ -21,18 +21,18 @@ code_loader/experiment_api/types.py,sha256=MY8xFARHwdVA7p4dxyhD60ShmttgTvb4qdp1o
21
21
  code_loader/experiment_api/utils.py,sha256=XZHtxge12TS4H4-8PjV3sKuhp8Ud6ojAiIzTZJEqBqc,3304
22
22
  code_loader/experiment_api/workingspace_config_utils.py,sha256=DLzXQCg4dgTV_YgaSbeTVzq-2ja_SQw4zi7LXwKL9cY,990
23
23
  code_loader/inner_leap_binder/__init__.py,sha256=koOlJyMNYzGbEsoIbXathSmQ-L38N_pEXH_HvL7beXU,99
24
- code_loader/inner_leap_binder/leapbinder.py,sha256=MsXT_1DcbCp7dQavHHe2zTEYwGJkIPTO4xaYPMYgsZg,55223
25
- code_loader/inner_leap_binder/leapbinder_decorators.py,sha256=InZSgWLrt977fIMbw_3WQE0MWhsQhnafhO3uZuMYc0I,176589
26
- code_loader/leaploader.py,sha256=2_t0IQPQNp9KcSKs4-_Ji8FK4Av8iouyK0eaRYNoUOM,69344
24
+ code_loader/inner_leap_binder/leapbinder.py,sha256=J-EjPz49kHqneaFhmgzid0MRokjxYb7kX6XkErxwtlc,58909
25
+ code_loader/inner_leap_binder/leapbinder_decorators.py,sha256=WE6gpZP8ckJp8OQ4CgWbz8BuF9KLwAeQYwDk-ua2jsY,186309
26
+ code_loader/leaploader.py,sha256=8q7JnsE1jx-PJNJN0oKak7i47X9dzenbz8lk-ezbjpw,88293
27
27
  code_loader/leaploaderbase.py,sha256=Aa8wCcxojf8EBn_14W5I1gKI4OMBdpdEcNAZ_6E2eV4,10940
28
28
  code_loader/mixpanel_tracker.py,sha256=rNwRmFifNbdUoqLQvvhhgpKczWpWiEmd8MfyJe27sxw,9131
29
29
  code_loader/plot_functions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
30
  code_loader/plot_functions/plot_functions.py,sha256=2DC-zlVaN13P4VNx5d8csgs80C6SisaeP1-Kq2LW7iM,16075
31
31
  code_loader/plot_functions/visualize.py,sha256=gsBAYYkwMh7jIpJeDMPS8G4CW-pxwx6LznoQIvi4vpo,657
32
- code_loader/utils.py,sha256=gEJCKpDWf6p9_KoEz-0EGiJqxD6QfPgZN6zdKdtSAz8,6628
32
+ code_loader/utils.py,sha256=Yj06BbS1UIjN9Cv2eQBjDfdDbpQlABAg6KwD3sB1BZ0,7496
33
33
  code_loader/visualizers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
34
  code_loader/visualizers/default_visualizers.py,sha256=onRnLE_TXfgLN4o52hQIOOhUcFexGlqJ3xSpQDVLuZM,2604
35
- code_loader-1.0.197.dev1.dist-info/LICENSE,sha256=qIwWjdspQeSMTtnFZBC8MuT-95L02FPvzRUdWFxrwJY,1067
36
- code_loader-1.0.197.dev1.dist-info/METADATA,sha256=USKyIvkEWJ6ak0J1mWQuFQ61R6BP1t3CUbfBlCDAVSo,1095
37
- code_loader-1.0.197.dev1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
38
- code_loader-1.0.197.dev1.dist-info/RECORD,,
35
+ code_loader-1.0.197.dev2.dist-info/LICENSE,sha256=qIwWjdspQeSMTtnFZBC8MuT-95L02FPvzRUdWFxrwJY,1067
36
+ code_loader-1.0.197.dev2.dist-info/METADATA,sha256=Ep3bwfV6ttp9_4BD5nL8a5zAzu0C3hauxJIkLisVuDU,1095
37
+ code_loader-1.0.197.dev2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
38
+ code_loader-1.0.197.dev2.dist-info/RECORD,,