ato 2.0.3__py3-none-any.whl → 2.1.1__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.

Potentially problematic release.


This version of ato might be problematic. Click here for more details.

@@ -0,0 +1,1021 @@
1
+ Metadata-Version: 2.4
2
+ Name: ato
3
+ Version: 2.1.1
4
+ Summary: Configuration, experimentation, and hyperparameter optimization for Python. No runtime magic. No launcher. Just Python modules you compose.
5
+ Author: ato contributors
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/yourusername/ato
8
+ Project-URL: Repository, https://github.com/yourusername/ato
9
+ Project-URL: Documentation, https://github.com/yourusername/ato#readme
10
+ Project-URL: Issues, https://github.com/yourusername/ato/issues
11
+ Keywords: config management,experiment tracking,hyperparameter optimization,lightweight,composable,namespace isolation,machine learning
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.7
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Requires-Python: >=3.7
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: pyyaml>=6.0
28
+ Requires-Dist: toml>=0.10.2
29
+ Requires-Dist: sqlalchemy>=2.0
30
+ Requires-Dist: numpy>=1.19.0
31
+ Provides-Extra: distributed
32
+ Requires-Dist: torch>=1.8.0; extra == "distributed"
33
+ Dynamic: license-file
34
+
35
+ # Ato: A Tiny Orchestrator
36
+
37
+ **Configuration, experimentation, and hyperparameter optimization for Python.**
38
+
39
+ No runtime magic. No launcher. No platform.
40
+ Just Python modules you compose.
41
+
42
+ ```bash
43
+ pip install ato
44
+ ```
45
+
46
+ ---
47
+
48
+ ## Design Philosophy
49
+
50
+ Ato was built on three constraints:
51
+
52
+ 1. **Visibility** — When configs merge from multiple sources, you should see **why** a value was set.
53
+ 2. **Composability** — Each module (ADict, Scope, SQLTracker, HyperOpt) works independently. Use one, use all, or mix with other tools.
54
+ 3. **Structural neutrality** — Ato is a layer, not a platform. It has no opinion on your stack.
55
+
56
+ This isn't minimalism for its own sake.
57
+ It's **structural restraint** — interfering only where necessary, staying out of the way everywhere else.
58
+
59
+ **What Ato provides:**
60
+ - **Config composition** with explicit priority, dependency chaining, and merge order debugging
61
+ - **Namespace isolation** for multi-team projects (MultiScope)
62
+ - **Experiment tracking** in local SQLite with zero setup
63
+ - **Hyperparameter search** via Hyperband (or compose with Optuna/Ray Tune)
64
+
65
+ **What Ato doesn't provide:**
66
+ - Web dashboards (use MLflow/W&B)
67
+ - Model registry (use MLflow)
68
+ - Dataset versioning (use DVC)
69
+ - Plugin marketplace
70
+
71
+ Ato is designed to work **between** tools, not replace them.
72
+
73
+ ---
74
+
75
+ ## Quick Start
76
+
77
+ ### 30-Second Example
78
+
79
+ ```python
80
+ from ato.scope import Scope
81
+
82
+ scope = Scope()
83
+
84
+ @scope.observe(default=True)
85
+ def config(config):
86
+ config.lr = 0.001
87
+ config.batch_size = 32
88
+ config.model = 'resnet50'
89
+
90
+ @scope
91
+ def train(config):
92
+ print(f"Training {config.model} with lr={config.lr}")
93
+ # Your training code here
94
+
95
+ if __name__ == '__main__':
96
+ train() # python train.py
97
+ # Override from CLI: python train.py lr=0.01 model=%resnet101%
98
+ ```
99
+
100
+ **Key features:**
101
+ - `@scope.observe()` defines config sources
102
+ - `@scope` injects the merged config
103
+ - CLI overrides work automatically
104
+ - Priority-based merging with dependency chaining (defaults → named configs → CLI → lazy evaluation)
105
+
106
+ ---
107
+
108
+ ## Table of Contents
109
+
110
+ - [ADict: Enhanced Dictionary](#adict-enhanced-dictionary)
111
+ - [Scope: Configuration Management](#scope-configuration-management)
112
+ - [Config Chaining](#config-chaining)
113
+ - [MultiScope: Namespace Isolation](#multiscope-namespace-isolation)
114
+ - [Config Documentation & Debugging](#configuration-documentation--debugging)
115
+ - [SQL Tracker: Experiment Tracking](#sql-tracker-experiment-tracking)
116
+ - [Hyperparameter Optimization](#hyperparameter-optimization)
117
+ - [Best Practices](#best-practices)
118
+ - [Contributing](#contributing)
119
+ - [Composability](#composability)
120
+
121
+ ---
122
+
123
+ ## ADict: Enhanced Dictionary
124
+
125
+ `ADict` is an enhanced dictionary for managing experiment configurations.
126
+
127
+ ### Core Features
128
+
129
+ | Feature | Description | Why It Matters |
130
+ |---------|-------------|----------------|
131
+ | **Structural Hashing** | Hash based on keys + types, not values | Track when experiment **structure** changes (not just hyperparameters) |
132
+ | **Nested Access** | Dot notation for nested configs | `config.model.lr` instead of `config['model']['lr']` |
133
+ | **Format Agnostic** | Load/save JSON, YAML, TOML, XYZ | Work with any config format |
134
+ | **Safe Updates** | `update_if_absent()` method | Merge configs without accidental overwrites |
135
+ | **Auto-nested** | `ADict.auto()` for lazy creation | `config.a.b.c = 1` just works - no KeyError |
136
+
137
+ ### Examples
138
+
139
+ #### Structural Hashing
140
+
141
+ ```python
142
+ from ato.adict import ADict
143
+
144
+ # Same structure, different values
145
+ config1 = ADict(lr=0.1, epochs=100, model='resnet50')
146
+ config2 = ADict(lr=0.01, epochs=200, model='resnet101')
147
+ print(config1.get_structural_hash() == config2.get_structural_hash()) # True
148
+
149
+ # Different structure (epochs is str!)
150
+ config3 = ADict(lr=0.1, epochs='100', model='resnet50')
151
+ print(config1.get_structural_hash() == config3.get_structural_hash()) # False
152
+ ```
153
+
154
+ #### Auto-nested Configs
155
+
156
+ ```python
157
+ # ❌ Traditional way
158
+ config = ADict()
159
+ config.model = ADict()
160
+ config.model.backbone = ADict()
161
+ config.model.backbone.layers = [64, 128, 256]
162
+
163
+ # ✅ With ADict.auto()
164
+ config = ADict.auto()
165
+ config.model.backbone.layers = [64, 128, 256] # Just works!
166
+ config.data.augmentation.brightness = 0.2
167
+ ```
168
+
169
+ #### Format Agnostic
170
+
171
+ ```python
172
+ # Load/save any format
173
+ config = ADict.from_file('config.json')
174
+ config.dump('config.yaml')
175
+
176
+ # Safe updates
177
+ config.update_if_absent(lr=0.01, scheduler='cosine') # Only adds scheduler
178
+ ```
179
+
180
+ ---
181
+
182
+ ## Scope: Configuration Management
183
+
184
+ Scope manages configuration through **priority-based merging** and **CLI integration**.
185
+
186
+ ### Key Concept: Priority Chain
187
+
188
+ ```
189
+ Default Configs (priority=0)
190
+
191
+ Named Configs (priority=0+)
192
+
193
+ CLI Arguments (highest priority)
194
+
195
+ Lazy Configs (computed after CLI)
196
+ ```
197
+
198
+ ### Basic Usage
199
+
200
+ #### Simple Configuration
201
+
202
+ ```python
203
+ from ato.scope import Scope
204
+
205
+ scope = Scope()
206
+
207
+ @scope.observe()
208
+ def my_config(config):
209
+ config.dataset = 'cifar10'
210
+ config.lr = 0.001
211
+ config.batch_size = 32
212
+
213
+ @scope
214
+ def train(config):
215
+ print(f"Training on {config.dataset}")
216
+ # Your code here
217
+
218
+ if __name__ == '__main__':
219
+ train()
220
+ ```
221
+
222
+ #### Priority-based Merging
223
+
224
+ ```python
225
+ @scope.observe(default=True) # Always applied
226
+ def defaults(config):
227
+ config.lr = 0.001
228
+ config.epochs = 100
229
+
230
+ @scope.observe(priority=1) # Applied after defaults
231
+ def high_lr(config):
232
+ config.lr = 0.01
233
+
234
+ @scope.observe(priority=2) # Applied last
235
+ def long_training(config):
236
+ config.epochs = 300
237
+ ```
238
+
239
+ ```bash
240
+ python train.py # lr=0.001, epochs=100
241
+ python train.py high_lr # lr=0.01, epochs=100
242
+ python train.py high_lr long_training # lr=0.01, epochs=300
243
+ ```
244
+
245
+ #### CLI Configuration
246
+
247
+ Override any parameter from command line:
248
+
249
+ ```bash
250
+ # Simple values
251
+ python train.py lr=0.01 batch_size=64
252
+
253
+ # Nested configs
254
+ python train.py model.backbone=%resnet101% model.depth=101
255
+
256
+ # Lists and complex types
257
+ python train.py layers=[64,128,256,512] dropout=0.5
258
+
259
+ # Combine with named configs
260
+ python train.py my_config lr=0.001 batch_size=128
261
+ ```
262
+
263
+ **Note**: Wrap strings with `%` (e.g., `%resnet101%`) instead of quotes.
264
+
265
+ ### Config Chaining
266
+
267
+ Sometimes configs have dependencies on other configs. Use `chain_with` to automatically apply prerequisite configs:
268
+
269
+ ```python
270
+ @scope.observe()
271
+ def base_setup(config):
272
+ config.project_name = 'my_project'
273
+ config.data_dir = '/data'
274
+
275
+ @scope.observe()
276
+ def gpu_setup(config):
277
+ config.device = 'cuda'
278
+ config.num_gpus = 4
279
+
280
+ @scope.observe(chain_with='base_setup') # Automatically applies base_setup first
281
+ def advanced_training(config):
282
+ config.distributed = True
283
+ config.mixed_precision = True
284
+
285
+ @scope.observe(chain_with=['base_setup', 'gpu_setup']) # Multiple dependencies
286
+ def multi_node_training(config):
287
+ config.nodes = 4
288
+ config.world_size = 16
289
+ ```
290
+
291
+ ```bash
292
+ # Calling advanced_training automatically applies base_setup first
293
+ python train.py advanced_training
294
+ # Results in: base_setup → advanced_training
295
+
296
+ # Calling multi_node_training applies all dependencies
297
+ python train.py multi_node_training
298
+ # Results in: base_setup → gpu_setup → multi_node_training
299
+ ```
300
+
301
+ **Why this matters:**
302
+ - **Explicit dependencies**: No more remembering to call prerequisite configs
303
+ - **Composable configs**: Build complex configs from simpler building blocks
304
+ - **Prevents errors**: Can't use a config without its dependencies
305
+
306
+ ### Lazy Evaluation
307
+
308
+ Sometimes you need configs that depend on other values set via CLI:
309
+
310
+ ```python
311
+ @scope.observe()
312
+ def base_config(config):
313
+ config.model = 'resnet50'
314
+ config.dataset = 'imagenet'
315
+
316
+ @scope.observe(lazy=True) # Evaluated AFTER CLI args
317
+ def computed_config(config):
318
+ # Adjust based on dataset
319
+ if config.dataset == 'imagenet':
320
+ config.num_classes = 1000
321
+ config.image_size = 224
322
+ elif config.dataset == 'cifar10':
323
+ config.num_classes = 10
324
+ config.image_size = 32
325
+ ```
326
+
327
+ ```bash
328
+ python train.py dataset=%cifar10% computed_config
329
+ # Results in: num_classes=10, image_size=32
330
+ ```
331
+
332
+ **Python 3.11+ Context Manager**:
333
+
334
+ ```python
335
+ @scope.observe()
336
+ def my_config(config):
337
+ config.model = 'resnet50'
338
+ config.num_layers = 50
339
+
340
+ with Scope.lazy(): # Evaluated after CLI
341
+ if config.model == 'resnet101':
342
+ config.num_layers = 101
343
+ ```
344
+
345
+ ### MultiScope: Namespace Isolation
346
+
347
+ Manage completely separate configuration namespaces with independent priority systems.
348
+
349
+ **Use case**: Different teams own different scopes without key collisions.
350
+
351
+ ```python
352
+ from ato.scope import Scope, MultiScope
353
+
354
+ model_scope = Scope(name='model')
355
+ data_scope = Scope(name='data')
356
+ scope = MultiScope(model_scope, data_scope)
357
+
358
+ @model_scope.observe(default=True)
359
+ def model_config(model):
360
+ model.backbone = 'resnet50'
361
+ model.lr = 0.1 # Model-specific learning rate
362
+
363
+ @data_scope.observe(default=True)
364
+ def data_config(data):
365
+ data.dataset = 'cifar10'
366
+ data.lr = 0.001 # Data augmentation learning rate (no conflict!)
367
+
368
+ @scope
369
+ def train(model, data): # Named parameters match scope names
370
+ # Both have 'lr' but in separate namespaces!
371
+ print(f"Model LR: {model.lr}, Data LR: {data.lr}")
372
+ ```
373
+
374
+ **Key advantage**: `model.lr` and `data.lr` are completely independent. No need for naming conventions like `model_lr` vs `data_lr`.
375
+
376
+ **CLI with MultiScope:**
377
+
378
+ ```bash
379
+ # Override model scope only
380
+ python train.py model.backbone=%resnet101%
381
+
382
+ # Override data scope only
383
+ python train.py data.dataset=%imagenet%
384
+
385
+ # Override both
386
+ python train.py model.backbone=%resnet101% data.dataset=%imagenet%
387
+ ```
388
+
389
+ ### Configuration Documentation & Debugging
390
+
391
+ **The `manual` command** visualizes the exact order of configuration application.
392
+
393
+ ```python
394
+ @scope.observe(default=True)
395
+ def config(config):
396
+ config.lr = 0.001
397
+ config.batch_size = 32
398
+ config.model = 'resnet50'
399
+
400
+ @scope.manual
401
+ def config_docs(config):
402
+ config.lr = 'Learning rate for optimizer'
403
+ config.batch_size = 'Number of samples per batch'
404
+ config.model = 'Model architecture (resnet50, resnet101, etc.)'
405
+ ```
406
+
407
+ ```bash
408
+ python train.py manual
409
+ ```
410
+
411
+ **Output:**
412
+ ```
413
+ --------------------------------------------------
414
+ [Scope "config"]
415
+ (The Applying Order of Views)
416
+ config → (CLI Inputs)
417
+
418
+ (User Manuals)
419
+ lr: Learning rate for optimizer
420
+ batch_size: Number of samples per batch
421
+ model: Model architecture (resnet50, resnet101, etc.)
422
+ --------------------------------------------------
423
+ ```
424
+
425
+ **Why this matters:**
426
+ When debugging "why is this config value not what I expect?", you can see **exactly** which function set it and in what order.
427
+
428
+ **Complex example:**
429
+
430
+ ```python
431
+ @scope.observe(default=True)
432
+ def defaults(config):
433
+ config.lr = 0.001
434
+
435
+ @scope.observe(priority=1)
436
+ def experiment_config(config):
437
+ config.lr = 0.01
438
+
439
+ @scope.observe(priority=2)
440
+ def another_config(config):
441
+ config.lr = 0.1
442
+
443
+ @scope.observe(lazy=True)
444
+ def adaptive_lr(config):
445
+ if config.batch_size > 64:
446
+ config.lr = config.lr * 2
447
+ ```
448
+
449
+ When you run `python train.py manual`, you see:
450
+ ```
451
+ (The Applying Order of Views)
452
+ defaults → experiment_config → another_config → (CLI Inputs) → adaptive_lr
453
+ ```
454
+
455
+ Now it's **crystal clear** why `lr=0.1` (from `another_config`) and not `0.01`!
456
+
457
+ ### Config Import/Export
458
+
459
+ ```python
460
+ @scope.observe()
461
+ def load_external(config):
462
+ # Load from any format
463
+ config.load('experiments/baseline.json')
464
+ config.load('models/resnet.yaml')
465
+
466
+ # Export to any format
467
+ config.dump('output/final_config.toml')
468
+ ```
469
+
470
+ **OpenMMLab compatibility:**
471
+
472
+ ```python
473
+ # Import OpenMMLab configs - handles _base_ inheritance automatically
474
+ config.load_mm_config('mmdet_configs/faster_rcnn.py')
475
+ ```
476
+
477
+ **Hierarchical composition:**
478
+
479
+ ```python
480
+ from ato.adict import ADict
481
+
482
+ # Load configs from directory structure
483
+ config = ADict.compose_hierarchy(
484
+ root='configs',
485
+ config_filename='config',
486
+ select={
487
+ 'model': 'resnet50',
488
+ 'data': 'imagenet'
489
+ },
490
+ overrides={
491
+ 'model.lr': 0.01,
492
+ 'data.batch_size': 64
493
+ },
494
+ required=['model.backbone', 'data.dataset'], # Validation
495
+ on_missing='warn' # or 'error'
496
+ )
497
+ ```
498
+
499
+ ### Argparse Integration
500
+
501
+ ```python
502
+ from ato.scope import Scope
503
+ import argparse
504
+
505
+ scope = Scope(use_external_parser=True)
506
+ parser = argparse.ArgumentParser()
507
+ parser.add_argument('--gpu', type=int, default=0)
508
+ parser.add_argument('--seed', type=int, default=42)
509
+
510
+ @scope.observe(default=True)
511
+ def config(config):
512
+ config.lr = 0.001
513
+ config.batch_size = 32
514
+
515
+ @scope
516
+ def train(config):
517
+ print(f"GPU: {config.gpu}, LR: {config.lr}")
518
+
519
+ if __name__ == '__main__':
520
+ parser.parse_args() # Merges argparse with scope
521
+ train()
522
+ ```
523
+
524
+ ---
525
+
526
+ ## SQL Tracker: Experiment Tracking
527
+
528
+ Lightweight experiment tracking using SQLite.
529
+
530
+ ### Why SQL Tracker?
531
+
532
+ - **Zero Setup**: Just a SQLite file, no servers
533
+ - **Full History**: Track all runs, metrics, and artifacts
534
+ - **Smart Search**: Find similar experiments by config structure
535
+ - **Code Versioning**: Track code changes via fingerprints
536
+ - **Offline-first**: No network required, sync to cloud tracking later if needed
537
+
538
+ ### Database Schema
539
+
540
+ ```
541
+ Project (my_ml_project)
542
+ ├── Experiment (run_1)
543
+ │ ├── config: {...}
544
+ │ ├── structural_hash: "abc123..."
545
+ │ ├── Metrics: [loss, accuracy, ...]
546
+ │ ├── Artifacts: [model.pt, plots/*, ...]
547
+ │ └── Fingerprints: [model_forward, train_step, ...]
548
+ ├── Experiment (run_2)
549
+ └── ...
550
+ ```
551
+
552
+ ### Usage
553
+
554
+ #### Logging Experiments
555
+
556
+ ```python
557
+ from ato.db_routers.sql.manager import SQLLogger
558
+ from ato.adict import ADict
559
+
560
+ # Setup config
561
+ config = ADict(
562
+ experiment=ADict(
563
+ project_name='image_classification',
564
+ sql=ADict(db_path='sqlite:///experiments.db')
565
+ ),
566
+ # Your hyperparameters
567
+ lr=0.001,
568
+ batch_size=32,
569
+ model='resnet50'
570
+ )
571
+
572
+ # Create logger
573
+ logger = SQLLogger(config)
574
+
575
+ # Start experiment run
576
+ run_id = logger.run(tags=['baseline', 'resnet50', 'cifar10'])
577
+
578
+ # Training loop
579
+ for epoch in range(100):
580
+ # Your training code
581
+ train_loss = train_one_epoch()
582
+ val_acc = validate()
583
+
584
+ # Log metrics
585
+ logger.log_metric('train_loss', train_loss, step=epoch)
586
+ logger.log_metric('val_accuracy', val_acc, step=epoch)
587
+
588
+ # Log artifacts
589
+ logger.log_artifact(run_id, 'checkpoints/model_best.pt',
590
+ data_type='model',
591
+ metadata={'epoch': best_epoch})
592
+
593
+ # Finish run
594
+ logger.finish(status='completed')
595
+ ```
596
+
597
+ #### Querying Experiments
598
+
599
+ ```python
600
+ from ato.db_routers.sql.manager import SQLFinder
601
+
602
+ finder = SQLFinder(config)
603
+
604
+ # Get all runs in project
605
+ runs = finder.get_runs_in_project('image_classification')
606
+ for run in runs:
607
+ print(f"Run {run.id}: {run.config.model} - {run.status}")
608
+
609
+ # Find best performing run
610
+ best_run = finder.find_best_run(
611
+ project_name='image_classification',
612
+ metric_key='val_accuracy',
613
+ mode='max' # or 'min' for loss
614
+ )
615
+ print(f"Best config: {best_run.config}")
616
+
617
+ # Find similar experiments (same config structure)
618
+ similar = finder.find_similar_runs(run_id=123)
619
+ print(f"Found {len(similar)} runs with similar config structure")
620
+
621
+ # Trace statistics (code fingerprints)
622
+ stats = finder.get_trace_statistics('image_classification', trace_id='model_forward')
623
+ print(f"Model forward pass has {stats['static_trace_versions']} versions")
624
+ ```
625
+
626
+ ### Features
627
+
628
+ | Feature | Description |
629
+ |---------|-------------|
630
+ | **Structural Hash** | Auto-track config structure changes |
631
+ | **Metric Logging** | Time-series metrics with step tracking |
632
+ | **Artifact Management** | Track model checkpoints, plots, data files |
633
+ | **Fingerprint Tracking** | Version control for code (static & runtime) |
634
+ | **Smart Search** | Find similar configs, best runs, statistics |
635
+
636
+ ---
637
+
638
+ ## Hyperparameter Optimization
639
+
640
+ Built-in **Hyperband** algorithm for efficient hyperparameter search with early stopping.
641
+
642
+ ### How Hyperband Works
643
+
644
+ Hyperband uses successive halving:
645
+ 1. Start with many configs, train briefly
646
+ 2. Keep top performers, discard poor ones
647
+ 3. Train survivors longer
648
+ 4. Repeat until one winner remains
649
+
650
+ ### Basic Usage
651
+
652
+ ```python
653
+ from ato.adict import ADict
654
+ from ato.hyperopt.hyperband import HyperBand
655
+ from ato.scope import Scope
656
+
657
+ scope = Scope()
658
+
659
+ # Define search space
660
+ search_spaces = ADict(
661
+ lr=ADict(
662
+ param_type='FLOAT',
663
+ param_range=(1e-5, 1e-1),
664
+ num_samples=20,
665
+ space_type='LOG' # Logarithmic spacing
666
+ ),
667
+ batch_size=ADict(
668
+ param_type='INTEGER',
669
+ param_range=(16, 128),
670
+ num_samples=5,
671
+ space_type='LOG'
672
+ ),
673
+ model=ADict(
674
+ param_type='CATEGORY',
675
+ categories=['resnet50', 'resnet101', 'efficientnet_b0']
676
+ )
677
+ )
678
+
679
+ # Create Hyperband optimizer
680
+ hyperband = HyperBand(
681
+ scope,
682
+ search_spaces,
683
+ halving_rate=0.3, # Keep top 30% each round
684
+ num_min_samples=3, # Stop when <= 3 configs remain
685
+ mode='max' # Maximize metric (use 'min' for loss)
686
+ )
687
+
688
+ @hyperband.main
689
+ def train(config):
690
+ # Your training code
691
+ model = create_model(config.model)
692
+ optimizer = Adam(lr=config.lr)
693
+
694
+ # Use __num_halved__ for early stopping
695
+ num_epochs = compute_epochs(config.__num_halved__)
696
+
697
+ # Train and return metric
698
+ val_acc = train_and_evaluate(model, optimizer, num_epochs)
699
+ return val_acc
700
+
701
+ if __name__ == '__main__':
702
+ # Run hyperparameter search
703
+ best_result = train()
704
+ print(f"Best config: {best_result.config}")
705
+ print(f"Best metric: {best_result.metric}")
706
+ ```
707
+
708
+ ### Automatic Step Calculation
709
+
710
+ ```python
711
+ hyperband = HyperBand(scope, search_spaces, halving_rate=0.3, num_min_samples=4)
712
+
713
+ max_steps = 100000
714
+ steps_per_generation = hyperband.compute_optimized_initial_training_steps(max_steps)
715
+ # Example output: [27, 88, 292, 972, 3240, 10800, 36000, 120000]
716
+
717
+ # Use in training
718
+ @hyperband.main
719
+ def train(config):
720
+ generation = config.__num_halved__
721
+ num_steps = steps_per_generation[generation]
722
+
723
+ metric = train_for_n_steps(num_steps)
724
+ return metric
725
+ ```
726
+
727
+ ### Parameter Types
728
+
729
+ | Type | Description | Example |
730
+ |------|-------------|---------|
731
+ | `FLOAT` | Continuous values | Learning rate, dropout |
732
+ | `INTEGER` | Discrete integers | Batch size, num layers |
733
+ | `CATEGORY` | Categorical choices | Model type, optimizer |
734
+
735
+ Space types:
736
+ - `LOG`: Logarithmic spacing (good for learning rates)
737
+ - `LINEAR`: Linear spacing (default)
738
+
739
+ ### Distributed Search
740
+
741
+ ```python
742
+ from ato.hyperopt.hyperband import DistributedHyperBand
743
+ import torch.distributed as dist
744
+
745
+ # Initialize distributed training
746
+ dist.init_process_group(backend='nccl')
747
+ rank = dist.get_rank()
748
+ world_size = dist.get_world_size()
749
+
750
+ # Create distributed hyperband
751
+ hyperband = DistributedHyperBand(
752
+ scope,
753
+ search_spaces,
754
+ halving_rate=0.3,
755
+ num_min_samples=3,
756
+ mode='max',
757
+ rank=rank,
758
+ world_size=world_size,
759
+ backend='pytorch'
760
+ )
761
+
762
+ @hyperband.main
763
+ def train(config):
764
+ # Your distributed training code
765
+ model = create_model(config)
766
+ model = DDP(model, device_ids=[rank])
767
+ metric = train_and_evaluate(model)
768
+ return metric
769
+
770
+ if __name__ == '__main__':
771
+ result = train()
772
+ if rank == 0:
773
+ print(f"Best config: {result.config}")
774
+ ```
775
+
776
+ ### Extensible Design
777
+
778
+ Ato's hyperopt module is built for extensibility:
779
+
780
+ | Component | Purpose |
781
+ |-----------|---------|
782
+ | `GridSpaceMixIn` | Parameter sampling logic (reusable) |
783
+ | `HyperOpt` | Base optimization class |
784
+ | `DistributedMixIn` | Distributed training support (optional) |
785
+
786
+ **Example: Implement custom search algorithm**
787
+
788
+ ```python
789
+ from ato.hyperopt.base import GridSpaceMixIn, HyperOpt
790
+
791
+ class RandomSearch(GridSpaceMixIn, HyperOpt):
792
+ def main(self, func):
793
+ # Reuse GridSpaceMixIn.prepare_distributions()
794
+ configs = self.prepare_distributions(self.config, self.search_spaces)
795
+
796
+ # Implement random sampling
797
+ import random
798
+ random.shuffle(configs)
799
+
800
+ results = []
801
+ for config in configs[:10]: # Sample 10 random configs
802
+ metric = func(config)
803
+ results.append((config, metric))
804
+
805
+ return max(results, key=lambda x: x[1])
806
+ ```
807
+
808
+ ---
809
+
810
+ ## Best Practices
811
+
812
+ ### 1. Project Structure
813
+
814
+ ```
815
+ my_project/
816
+ ├── configs/
817
+ │ ├── default.py # Default config with @scope.observe(default=True)
818
+ │ ├── models.py # Model-specific configs
819
+ │ └── datasets.py # Dataset configs
820
+ ├── train.py # Main training script
821
+ ├── experiments.db # SQLite experiment tracking
822
+ └── experiments/
823
+ ├── run_001/
824
+ │ ├── checkpoints/
825
+ │ └── logs/
826
+ └── run_002/
827
+ ```
828
+
829
+ ### 2. Config Organization
830
+
831
+ ```python
832
+ # configs/default.py
833
+ from ato.scope import Scope
834
+ from ato.adict import ADict
835
+
836
+ scope = Scope()
837
+
838
+ @scope.observe(default=True)
839
+ def defaults(config):
840
+ # Data
841
+ config.data = ADict(
842
+ dataset='cifar10',
843
+ batch_size=32,
844
+ num_workers=4
845
+ )
846
+
847
+ # Model
848
+ config.model = ADict(
849
+ backbone='resnet50',
850
+ pretrained=True
851
+ )
852
+
853
+ # Training
854
+ config.train = ADict(
855
+ lr=0.001,
856
+ epochs=100,
857
+ optimizer='adam'
858
+ )
859
+
860
+ # Experiment tracking
861
+ config.experiment = ADict(
862
+ project_name='my_project',
863
+ sql=ADict(db_path='sqlite:///experiments.db')
864
+ )
865
+ ```
866
+
867
+ ### 3. Combined Workflow
868
+
869
+ ```python
870
+ from ato.scope import Scope
871
+ from ato.db_routers.sql.manager import SQLLogger
872
+ from configs.default import scope
873
+
874
+ @scope
875
+ def train(config):
876
+ # Setup experiment tracking
877
+ logger = SQLLogger(config)
878
+ run_id = logger.run(tags=[config.model.backbone, config.data.dataset])
879
+
880
+ try:
881
+ # Training loop
882
+ for epoch in range(config.train.epochs):
883
+ loss = train_epoch()
884
+ acc = validate()
885
+
886
+ logger.log_metric('loss', loss, epoch)
887
+ logger.log_metric('accuracy', acc, epoch)
888
+
889
+ logger.finish(status='completed')
890
+
891
+ except Exception as e:
892
+ logger.finish(status='failed')
893
+ raise e
894
+
895
+ if __name__ == '__main__':
896
+ train()
897
+ ```
898
+
899
+ ### 4. Reproducibility Checklist
900
+
901
+ - ✅ Use structural hashing to track config changes
902
+ - ✅ Log all hyperparameters to SQLLogger
903
+ - ✅ Tag experiments with meaningful labels
904
+ - ✅ Track artifacts (checkpoints, plots)
905
+ - ✅ Use lazy configs for derived parameters
906
+ - ✅ Document configs with `@scope.manual`
907
+
908
+ ---
909
+
910
+ ## Requirements
911
+
912
+ - Python >= 3.7
913
+ - SQLAlchemy (for SQL Tracker)
914
+ - PyYAML, toml (for config serialization)
915
+
916
+ See `pyproject.toml` for full dependencies.
917
+
918
+ ---
919
+
920
+ ## Contributing
921
+
922
+ Contributions are welcome! Please feel free to submit issues or pull requests.
923
+
924
+ ### Development Setup
925
+
926
+ ```bash
927
+ git clone https://github.com/yourusername/ato.git
928
+ cd ato
929
+ pip install -e .
930
+ ```
931
+
932
+ ### Quality Assurance
933
+
934
+ Ato's design philosophy — **structural neutrality** and **debuggable composition** — extends to our testing practices.
935
+
936
+ **Release Policy:**
937
+ - **All 100+ unit tests must pass before any release**
938
+ - No exceptions, no workarounds
939
+ - Tests cover every module: ADict, Scope, MultiScope, SQLTracker, HyperBand
940
+
941
+ **Why this matters:**
942
+ When you build on Ato, you're trusting it to stay out of your way. That means zero regressions, predictable behavior, and reliable APIs. Comprehensive test coverage ensures that each component works independently and composes correctly.
943
+
944
+ Run tests locally:
945
+ ```bash
946
+ python -m pytest unit_tests/
947
+ ```
948
+
949
+ ---
950
+
951
+ ## Composability
952
+
953
+ Ato is designed to **compose** with existing tools, not replace them.
954
+
955
+ ### Works Where Other Systems Require Ecosystems
956
+
957
+ **Config composition:**
958
+ - Import OpenMMLab configs: `config.load_mm_config('mmdet_configs/faster_rcnn.py')`
959
+ - Load Hydra-style hierarchies: `ADict.compose_hierarchy(root='configs', select={'model': 'resnet50'})`
960
+ - Mix with argparse: `Scope(use_external_parser=True)`
961
+
962
+ **Experiment tracking:**
963
+ - Track locally in SQLite (zero setup)
964
+ - Sync to MLflow/W&B when you need dashboards
965
+ - Or use both: local SQLite + cloud tracking
966
+
967
+ **Hyperparameter optimization:**
968
+ - Built-in Hyperband
969
+ - Or compose with Optuna/Ray Tune — Ato's configs work with any optimizer
970
+
971
+ ### Four Capabilities Other Tools Don't Provide
972
+
973
+ 1. **Config chaining (`chain_with`)** — Explicit dependency management between configs
974
+ 2. **MultiScope** — True namespace isolation with independent priority systems
975
+ 3. **`manual` command** — Visualize exact config merge order for debugging
976
+ 4. **Structural hashing** — Track when experiment **architecture** changes, not just values
977
+
978
+ ### When to Use Ato
979
+
980
+ **Use Ato when:**
981
+ - You want zero boilerplate config management
982
+ - You need to debug why a config value isn't what you expect
983
+ - You're working on multi-team projects with namespace conflicts
984
+ - You want local-first experiment tracking
985
+ - You're migrating between config/tracking systems
986
+
987
+ **Ato works alongside:**
988
+ - Hydra (config composition)
989
+ - MLflow/W&B (cloud tracking)
990
+ - Optuna/Ray Tune (advanced hyperparameter search)
991
+ - PyTorch/TensorFlow/JAX (any ML framework)
992
+
993
+ ---
994
+
995
+ ## Roadmap
996
+
997
+ Ato's design constraint is **structural neutrality** — adding capabilities without creating dependencies.
998
+
999
+ ### Planned: Local Dashboard (Optional Module)
1000
+
1001
+ A lightweight HTML dashboard for teams that want visual exploration without committing to cloud platforms:
1002
+
1003
+ **What it adds:**
1004
+ - Metric comparison & trends (read-only view of SQLite data)
1005
+ - Run history & artifact browsing
1006
+ - Config diff visualization
1007
+ - Interactive hyperparameter analysis
1008
+
1009
+ **Design constraints:**
1010
+ - No hard dependency — Ato core works 100% without the dashboard
1011
+ - Separate process — doesn't block or modify runs
1012
+ - Zero lock-in — delete it anytime, training code doesn't change
1013
+ - Composable — use alongside MLflow/W&B
1014
+
1015
+ **Guiding principle:** Ato remains a set of **independent, composable tools** — not a platform you commit to.
1016
+
1017
+ ---
1018
+
1019
+ ## License
1020
+
1021
+ MIT License