code-loader 1.0.197.dev0__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.
@@ -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
@@ -1621,38 +1702,58 @@ def tensorleap_metadata(
1621
1702
  return decorating_function
1622
1703
 
1623
1704
 
1624
- def tensorleap_custom_latent_space(name: Optional[str] = None):
1705
+ def tensorleap_custom_latent_space(name: Optional[str] = None, use_ls_for_analysis: bool = False):
1706
+ assert isinstance(use_ls_for_analysis, bool), \
1707
+ ("tensorleap_custom_latent_space validation failed: use_ls_for_analysis must be a bool. "
1708
+ f"Got {type(use_ls_for_analysis)}.")
1709
+
1625
1710
  def decorating_function(user_function: SectionCallableInterface):
1626
1711
  ls_name = name if name is not None else user_function.__name__
1627
- def _validate_input_args(sample_id: Union[int, str], preprocess_response: PreprocessResponse):
1628
- assert isinstance(sample_id, (int, str)), \
1629
- (f'tensorleap_custom_latent_space validation failed: '
1630
- f'Argument sample_id should be either int or str. Got {type(sample_id)}.')
1631
- assert isinstance(preprocess_response, PreprocessResponse), \
1632
- (f'tensorleap_custom_latent_space validation failed: '
1633
- f'Argument preprocess_response should be a PreprocessResponse. Got {type(preprocess_response)}.')
1634
- assert type(sample_id) == preprocess_response.sample_id_type, \
1635
- (f'tensorleap_custom_latent_space validation failed: '
1636
- f'Argument sample_id should be as the same type as defined in the preprocess response '
1637
- f'{preprocess_response.sample_id_type}. Got {type(sample_id)}.')
1638
1712
 
1639
- def _validate_result(result):
1640
- 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), \
1641
1718
  (f'tensorleap_custom_latent_space validation failed: '
1642
- f'The return type should be a numpy array. Got {type(result)}.')
1643
- if result.ndim > 1:
1644
- 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))
1645
1722
  store_general_warning(
1646
- key=("tensorleap_custom_latent_space_flatten", ls_name, tuple(result.shape)),
1723
+ key=("tensorleap_custom_latent_space_flatten", ls_name, tuple(single_result.shape)),
1647
1724
  message=(
1648
- f"tensorleap_custom_latent_space '{ls_name}' returned per-sample shape {tuple(result.shape)} "
1649
- 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 "
1650
1727
  f"flatten to ({flat_dim},) before downstream visualization and clustering. "
1651
1728
  f"If you want a different aggregation (e.g. global average pooling), do it "
1652
1729
  f"inside your function."
1653
1730
  ),
1654
1731
  )
1655
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
+
1656
1757
  def inner_without_validate(sample_id, preprocess_response):
1657
1758
  global _called_from_inside_tl_decorator
1658
1759
  _called_from_inside_tl_decorator += 1
@@ -1664,7 +1765,8 @@ def tensorleap_custom_latent_space(name: Optional[str] = None):
1664
1765
 
1665
1766
  return result
1666
1767
 
1667
- leap_binder.set_custom_latent_space(inner_without_validate, ls_name)
1768
+ leap_binder.set_custom_latent_space(inner_without_validate, ls_name,
1769
+ use_ls_for_analysis=use_ls_for_analysis)
1668
1770
 
1669
1771
  def inner(sample_id, preprocess_response):
1670
1772
  if os.environ.get(mapping_runtime_mode_env_var_mame):
@@ -1672,9 +1774,11 @@ def tensorleap_custom_latent_space(name: Optional[str] = None):
1672
1774
 
1673
1775
  _validate_input_args(sample_id, preprocess_response)
1674
1776
 
1777
+ grouped = isinstance(sample_id, list)
1778
+ group_size = len(sample_id) if grouped else None
1675
1779
  result = inner_without_validate(sample_id, preprocess_response)
1676
1780
 
1677
- _validate_result(result)
1781
+ _validate_result(result, grouped, group_size)
1678
1782
  return result
1679
1783
 
1680
1784
  return inner
@@ -2643,6 +2747,15 @@ def tensorleap_preprocess():
2643
2747
  assert len(set(result)) == len(result), \
2644
2748
  (f'{user_function.__name__}() validation failed: '
2645
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}.')
2646
2759
 
2647
2760
  def inner(*args, **kwargs):
2648
2761
  if not _call_from_tl_platform:
@@ -2760,6 +2873,13 @@ def tensorleap_element_instance_preprocess(
2760
2873
  result = user_function()
2761
2874
  found_instance_metadata = False
2762
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.")
2763
2883
  sample_ids_to_instance_mappings = {}
2764
2884
  instance_to_sample_ids_mappings = {}
2765
2885
  all_sample_ids = preprocess_response.sample_ids.copy()
@@ -2980,14 +3100,10 @@ def tensorleap_input_encoder(name: str, channel_dim=_UNSET, model_input_index=No
2980
3100
  f"channel axis in the batched tensor (batch = axis 0), so 0 is invalid. Use 1 "
2981
3101
  f"for channel-first (NCHW) and -1 for channel-last (NHWC).")
2982
3102
 
2983
- def _validate_input_args(sample_id: Union[int, str], preprocess_response: PreprocessResponse):
2984
- assert type(sample_id) == preprocess_response.sample_id_type, \
2985
- (f'{user_function.__name__}() validation failed: '
2986
- f'Argument sample_id should be as the same type as defined in the preprocess response '
2987
- 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__)
2988
3105
 
2989
- def _validate_result(result):
2990
- validate_output_structure(result, func_name=user_function.__name__, expected_type_name="np.ndarray")
3106
+ def _validate_single(result):
2991
3107
  assert isinstance(result, np.ndarray), \
2992
3108
  (f'{user_function.__name__}() validation failed: '
2993
3109
  f'Unsupported return type. Should be a numpy array. Got {type(result)}.')
@@ -2997,6 +3113,13 @@ def tensorleap_input_encoder(name: str, channel_dim=_UNSET, model_input_index=No
2997
3113
  assert channel_dim - 1 <= len(result.shape), (f'{user_function.__name__}() validation failed: '
2998
3114
  f'The channel_dim ({channel_dim}) should be <= to the rank of the resulting input rank ({len(result.shape)}).')
2999
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
+
3000
3123
  def inner_without_validate(sample_id, preprocess_response):
3001
3124
  global _called_from_inside_tl_decorator
3002
3125
  _called_from_inside_tl_decorator += 1
@@ -3013,18 +3136,29 @@ def tensorleap_input_encoder(name: str, channel_dim=_UNSET, model_input_index=No
3013
3136
  def inner(*args, **kwargs):
3014
3137
  if not _call_from_tl_platform:
3015
3138
  set_current("tensorleap_input_encoder")
3016
- validate_args_structure(*args, types_order=[Union[int, str], PreprocessResponse],
3139
+ validate_args_structure(*args, types_order=[Union[int, str, list], PreprocessResponse],
3017
3140
  func_name=user_function.__name__, expected_names=["idx", "preprocess"], **kwargs)
3018
3141
  sample_id, preprocess_response = args if len(args) != 0 else kwargs.values()
3019
3142
  _validate_input_args(sample_id, preprocess_response)
3020
3143
 
3144
+ grouped = isinstance(sample_id, list)
3145
+ group_size = len(sample_id) if grouped else None
3021
3146
  result = inner_without_validate(sample_id, preprocess_response)
3022
3147
 
3023
- _validate_result(result)
3148
+ _validate_result(result, grouped, group_size)
3024
3149
 
3025
3150
  if _called_from_inside_tl_decorator == 0 and _called_from_inside_tl_integration_test_decorator:
3026
- batch_warning(result, user_function.__name__)
3027
- 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)
3028
3162
  # Emit integration test event once per test
3029
3163
  try:
3030
3164
  emit_integration_event_once(AnalyticsEvent.INPUT_ENCODER_INTEGRATION_TEST, {
@@ -3045,8 +3179,15 @@ def tensorleap_input_encoder(name: str, channel_dim=_UNSET, model_input_index=No
3045
3179
  inner.node_mapping = NodeMapping(name, node_mapping_type)
3046
3180
 
3047
3181
  def mapping_inner(*args, **kwargs):
3048
- class TempMapping:
3049
- 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
3050
3191
 
3051
3192
  ret = TempMapping()
3052
3193
  ret.node_mapping = mapping_inner.node_mapping
@@ -3076,15 +3217,10 @@ def tensorleap_gt_encoder(name: str):
3076
3217
  raise Exception(f'GT with name {name} already exists. '
3077
3218
  f'Please choose another')
3078
3219
 
3079
- def _validate_input_args(sample_id: Union[int, str], preprocess_response: PreprocessResponse):
3080
- assert type(sample_id) == preprocess_response.sample_id_type, \
3081
- (f'{user_function.__name__}() validation failed: '
3082
- f'Argument sample_id should be as the same type as defined in the preprocess response '
3083
- 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__)
3084
3222
 
3085
- def _validate_result(result):
3086
- validate_output_structure(result, func_name=user_function.__name__, expected_type_name="np.ndarray",
3087
- gt_flag=True)
3223
+ def _validate_single(result):
3088
3224
  assert isinstance(result, np.ndarray), \
3089
3225
  (f'{user_function.__name__}() validation failed: '
3090
3226
  f'Unsupported return type. Should be a numpy array. Got {type(result)}.')
@@ -3092,6 +3228,14 @@ def tensorleap_gt_encoder(name: str):
3092
3228
  (f'{user_function.__name__}() validation failed: '
3093
3229
  f'The return type should be a numpy array of type float32. Got {result.dtype}.')
3094
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
+
3095
3239
  def inner_without_validate(sample_id, preprocess_response):
3096
3240
  global _called_from_inside_tl_decorator
3097
3241
  _called_from_inside_tl_decorator += 1
@@ -3108,18 +3252,29 @@ def tensorleap_gt_encoder(name: str):
3108
3252
  def inner(*args, **kwargs):
3109
3253
  if not _call_from_tl_platform:
3110
3254
  set_current("tensorleap_gt_encoder")
3111
- validate_args_structure(*args, types_order=[Union[int, str], PreprocessResponse],
3255
+ validate_args_structure(*args, types_order=[Union[int, str, list], PreprocessResponse],
3112
3256
  func_name=user_function.__name__, expected_names=["idx", "preprocess"], **kwargs)
3113
- sample_id, preprocess_response = args
3257
+ sample_id, preprocess_response = args if len(args) != 0 else kwargs.values()
3114
3258
  _validate_input_args(sample_id, preprocess_response)
3115
3259
 
3260
+ grouped = isinstance(sample_id, list)
3261
+ group_size = len(sample_id) if grouped else None
3116
3262
  result = inner_without_validate(sample_id, preprocess_response)
3117
3263
 
3118
- _validate_result(result)
3264
+ _validate_result(result, grouped, group_size)
3119
3265
 
3120
3266
  if _called_from_inside_tl_decorator == 0 and _called_from_inside_tl_integration_test_decorator:
3121
- batch_warning(result, user_function.__name__)
3122
- 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)
3123
3278
  _register_chain_artifact(result, 'ground_truth')
3124
3279
  # Emit integration test event once per test
3125
3280
  try:
@@ -3135,8 +3290,15 @@ def tensorleap_gt_encoder(name: str):
3135
3290
  inner.node_mapping = NodeMapping(name, NodeMappingType.GroundTruth)
3136
3291
 
3137
3292
  def mapping_inner(*args, **kwargs):
3138
- class TempMapping:
3139
- 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
3140
3302
 
3141
3303
  ret = TempMapping()
3142
3304
  ret.node_mapping = mapping_inner.node_mapping