Skip to content

Commit 9822458

Browse files
authored
Merge pull request #24 from DocGarbanzo/claude/remove-hardcoded-tub-fields-y1gqh
Remove hardcoded tub fields and silent fallbacks from TubStatistics
2 parents 87d36f9 + ef24806 commit 9822458

10 files changed

+344
-149
lines changed

donkeycar/parts/tub_statistics.py

Lines changed: 39 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -187,46 +187,35 @@ class TubStatistics(object):
187187
def __init__(self,
188188
tub: Tub,
189189
config: Optional[Any] = None,
190-
gyro_z_index: int = 1,
191190
sorting_strategy: Optional[SortingStrategy] = None,
192191
field_aggregations: Optional[List] = None):
193192
"""
194193
Construct tub statistics calculator for tub
195194
196195
:param tub: input tub
197196
:param config: Config object (loads FIELD_AGGREGATIONS,
198-
LAP_SORTING_CRITERIA)
199-
:param gyro_z_index: z coordinate in 3d gyro vector (deprecated,
200-
use config)
197+
LAP_SORTING_CRITERIA). Required if
198+
field_aggregations not provided.
201199
:param sorting_strategy: Optional custom sorting strategy
202-
(backward compat)
203200
:param field_aggregations: Optional list of FieldAggregationSpec or
204-
old-style dicts (backward compat)
201+
dicts. Required if config not provided.
202+
:raises ValueError: If neither config nor field_aggregations
203+
provided.
205204
"""
206205
self.tub = tub
207206

208-
# Load field aggregations with precedence:
209-
# 1. Direct parameter (backward compat)
210-
# 2. From config (new preferred method)
211-
# 3. Default fallback
207+
# Load field aggregations - require explicit configuration
212208
if field_aggregations is not None:
213-
# Handle both old-style dicts and new FieldAggregationSpec
214209
self.field_aggregations = self._normalize_field_aggregations(
215-
field_aggregations, gyro_z_index)
210+
field_aggregations)
216211
elif config:
217212
self.field_aggregations = (
218213
self._load_field_aggregations_from_config(config))
219214
else:
220-
# Backward compatible defaults
221-
self.field_aggregations = [
222-
FieldAggregationSpec(
223-
field='car/gyro',
224-
output_key='gyro_z_agg',
225-
index=gyro_z_index,
226-
transform=abs,
227-
aggregation='avg'
228-
)
229-
]
215+
raise ValueError(
216+
'TubStatistics requires either config or field_aggregations. '
217+
'No silent defaults are used - configure FIELD_AGGREGATIONS '
218+
'in config or pass field_aggregations directly.')
230219

231220
# Load sorting strategy
232221
if sorting_strategy:
@@ -240,58 +229,45 @@ def __init__(self,
240229
logger.info(f'Creating TubStatistics with '
241230
f'{len(self.field_aggregations)} field aggregations')
242231

243-
def _normalize_field_aggregations(self, field_aggregations: List,
244-
gyro_z_index: int) -> List[
232+
def _normalize_field_aggregations(self, field_aggregations: List) -> List[
245233
FieldAggregationSpec]:
246-
"""Convert old-style dict specs to FieldAggregationSpec."""
234+
"""Convert dict specs to FieldAggregationSpec.
235+
236+
:raises ValueError: If old-style extractor syntax is used.
237+
"""
247238
normalized = []
248239
for spec in field_aggregations:
249240
if isinstance(spec, FieldAggregationSpec):
250241
normalized.append(spec)
251242
elif isinstance(spec, dict):
252-
# Old style dict with 'extractor' and 'transform'
253243
if 'extractor' in spec:
254-
# Cannot convert old-style extractor lambdas,
255-
# use gyro_z_index default
256-
logger.warning('Old-style field_aggregations dict with '
257-
'extractor not supported. Using defaults.')
258-
normalized.append(FieldAggregationSpec(
259-
field=spec['field'],
260-
output_key=spec['output_key'],
261-
index=gyro_z_index,
262-
transform=spec.get('transform'),
263-
aggregation='avg'
264-
))
265-
else:
266-
# New style dict
267-
normalized.append(FieldAggregationSpec(
268-
field=spec['field'],
269-
output_key=spec['output_key'],
270-
index=spec.get('index'),
271-
transform=spec.get('transform'),
272-
aggregation=spec.get('aggregation', 'avg')
273-
))
244+
raise ValueError(
245+
f'Old-style field_aggregations with "extractor" '
246+
f'not supported for field {spec.get("field", "?")}. '
247+
f'Use "index" parameter instead.')
248+
normalized.append(FieldAggregationSpec(
249+
field=spec['field'],
250+
output_key=spec['output_key'],
251+
index=spec.get('index'),
252+
transform=spec.get('transform'),
253+
aggregation=spec.get('aggregation', 'avg')
254+
))
274255
return normalized
275256

276257
def _load_field_aggregations_from_config(self, config) -> List[
277258
FieldAggregationSpec]:
278-
"""Load field aggregation specs from config."""
259+
"""Load field aggregation specs from config.
260+
261+
:raises ValueError: If FIELD_AGGREGATIONS not found in config.
262+
"""
279263
config_specs = getattr(config, 'FIELD_AGGREGATIONS', None)
280264

281265
if not config_specs:
282-
# Fallback to legacy GYRO_Z_INDEX
283-
gyro_z_index = getattr(config, 'GYRO_Z_INDEX', 1)
284-
logger.info(f'No FIELD_AGGREGATIONS in config, using '
285-
f'default gyro aggregation with index {gyro_z_index}')
286-
return [
287-
FieldAggregationSpec(
288-
field='car/gyro',
289-
output_key='gyro_z_agg',
290-
index=gyro_z_index,
291-
transform=abs,
292-
aggregation='avg'
293-
)
294-
]
266+
raise ValueError(
267+
'FIELD_AGGREGATIONS not found in config. '
268+
'Please define FIELD_AGGREGATIONS in your config file. '
269+
'Example: FIELD_AGGREGATIONS = [{"field": "car/gyro", '
270+
'"output_key": "gyro_z_agg", "index": 1, "aggregation": "avg"}]')
295271

296272
# Convert config dicts to FieldAggregationSpec
297273
specs = []
@@ -317,7 +293,9 @@ def _load_sorting_strategy_from_config(self, config) -> SortingStrategy:
317293
f'{[c["key"] for c in criteria]}')
318294
return SortingStrategy(criteria)
319295
else:
320-
logger.info('No LAP_SORTING_CRITERIA in config, using defaults')
296+
logger.info('No LAP_SORTING_CRITERIA in config, using minimal '
297+
'defaults (time, distance). Configure LAP_SORTING_CRITERIA '
298+
'in config to include custom fields like gyro_z_agg.')
321299
return default_lap_sorting_strategy()
322300

323301
def generate_laptimes_from_records(self, overwrite=False):

donkeycar/pipeline/transformations.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,15 @@ def get_sort_value(item):
109109

110110
def default_lap_sorting_strategy() -> SortingStrategy:
111111
"""
112-
Create default lap sorting strategy (backward compatible).
112+
Create default lap sorting strategy.
113113
114-
Sorts by: time, distance, gyro_z_agg (all ascending).
114+
Sorts by: time and distance (all ascending).
115+
Only includes fields that are always computed from lap timing.
116+
117+
Note: For custom field rankings (e.g., gyro_z_agg), configure
118+
LAP_SORTING_CRITERIA in your config file.
115119
"""
116120
return SortingStrategy([
117121
{'key': 'time'},
118122
{'key': 'distance'},
119-
{'key': 'gyro_z_agg'},
120123
])

donkeycar/tests/test_course_segmentation_integration.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,22 @@
1616

1717
from donkeycar.config import Config
1818
from donkeycar.parts.tub_v2 import Tub
19-
from donkeycar.parts.tub_statistics import TubStatistics
19+
from donkeycar.parts.tub_statistics import TubStatistics, FieldAggregationSpec
2020
from donkeycar.pipeline.types import TubDataset, PctMode
2121

2222

23+
# Standard field aggregation for gyro_z at index 1 (simulator convention)
24+
GYRO_Z_INDEX_1 = [
25+
FieldAggregationSpec(
26+
field='car/gyro',
27+
output_key='gyro_z_agg',
28+
index=1,
29+
transform=abs,
30+
aggregation='avg'
31+
)
32+
]
33+
34+
2335
class TestSegmentationTrainingIntegration(unittest.TestCase):
2436
"""Test segmentation integration with training pipeline"""
2537

@@ -47,7 +59,19 @@ def _create_test_config(self):
4759
cfg = Config()
4860
cfg.USE_LAP_0 = False
4961
cfg.TRAIN_TEST_SPLIT = 0.8
50-
cfg.GYRO_Z_INDEX = 1
62+
cfg.FIELD_AGGREGATIONS = [
63+
{
64+
'field': 'car/gyro',
65+
'output_key': 'gyro_z_agg',
66+
'index': 1,
67+
'aggregation': 'avg'
68+
}
69+
]
70+
cfg.LAP_SORTING_CRITERIA = [
71+
{'key': 'time'},
72+
{'key': 'distance'},
73+
{'key': 'gyro_z_agg'},
74+
]
5175
return cfg
5276

5377
def _create_test_tub(self):
@@ -211,7 +235,7 @@ def test_lap_mode_still_works(self):
211235
# Need to compute lap performance first
212236
tub = Tub(self.tub_path, read_only=True)
213237
self.open_tubs.append(tub)
214-
stats = TubStatistics(tub)
238+
stats = TubStatistics(tub, field_aggregations=GYRO_Z_INDEX_1)
215239

216240
# This would normally be done by generate_laptimes_from_records
217241
# but we created metadata manually, so just verify it exists

donkeycar/tests/test_lap_pct_regression.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,28 @@
1616

1717
from donkeycar.config import Config
1818
from donkeycar.parts.tub_v2 import Tub
19-
from donkeycar.parts.tub_statistics import TubStatistics
19+
from donkeycar.parts.tub_statistics import TubStatistics, FieldAggregationSpec
2020
from donkeycar.pipeline.types import TubDataset, PctMode
21+
from donkeycar.pipeline.transformations import SortingStrategy
22+
23+
24+
# Standard field aggregation for gyro_z at index 1 (simulator convention)
25+
GYRO_Z_INDEX_1 = [
26+
FieldAggregationSpec(
27+
field='car/gyro',
28+
output_key='gyro_z_agg',
29+
index=1,
30+
transform=abs,
31+
aggregation='avg'
32+
)
33+
]
34+
35+
# Sorting strategy that includes time, distance, and gyro_z_agg
36+
FULL_SORTING_STRATEGY = SortingStrategy([
37+
{'key': 'time'},
38+
{'key': 'distance'},
39+
{'key': 'gyro_z_agg'},
40+
])
2141

2242

2343
class TestLapPerformanceRegression(unittest.TestCase):
@@ -46,7 +66,14 @@ def _create_test_config(self):
4666
cfg = Config()
4767
cfg.USE_LAP_0 = False
4868
cfg.TRAIN_TEST_SPLIT = 0.8
49-
cfg.GYRO_Z_INDEX = 1
69+
cfg.FIELD_AGGREGATIONS = [
70+
{
71+
'field': 'car/gyro',
72+
'output_key': 'gyro_z_agg',
73+
'index': 1,
74+
'aggregation': 'avg'
75+
}
76+
]
5077
return cfg
5178

5279
def test_lap_mode_unchanged(self):
@@ -115,7 +142,8 @@ def test_lap_performance_calculation_unchanged(self):
115142

116143
tub = Tub(self.tub_path, read_only=True)
117144
self.open_tubs.append(tub)
118-
stats = TubStatistics(tub)
145+
stats = TubStatistics(tub, field_aggregations=GYRO_Z_INDEX_1,
146+
sorting_strategy=FULL_SORTING_STRATEGY)
119147

120148
# Calculate lap performance (original method)
121149
session_lap_rank = stats.calculate_lap_performance()

0 commit comments

Comments
 (0)