ato 2.0.0__tar.gz → 2.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

@@ -1,14 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ato
3
- Version: 2.0.0
4
- Summary: A Python library for experiment tracking and hyperparameter optimization
3
+ Version: 2.1.0
4
+ Summary: Configuration, experimentation, and hyperparameter optimization for Python. No runtime magic. No launcher. Just Python modules you compose.
5
5
  Author: ato contributors
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/yourusername/ato
8
8
  Project-URL: Repository, https://github.com/yourusername/ato
9
9
  Project-URL: Documentation, https://github.com/yourusername/ato#readme
10
10
  Project-URL: Issues, https://github.com/yourusername/ato/issues
11
- Keywords: machine learning,experiment tracking,hyperparameter optimization
11
+ Keywords: config management,experiment tracking,hyperparameter optimization,lightweight,composable,namespace isolation,machine learning
12
12
  Classifier: Development Status :: 4 - Beta
13
13
  Classifier: Intended Audience :: Developers
14
14
  Classifier: Intended Audience :: Science/Research
@@ -32,38 +32,47 @@ Provides-Extra: distributed
32
32
  Requires-Dist: torch>=1.8.0; extra == "distributed"
33
33
  Dynamic: license-file
34
34
 
35
- # Ato
35
+ # Ato: A Tiny Orchestrator
36
36
 
37
- Ato is intentionally small it’s not about lines of code,
38
- it’s about where they belong.
39
- The core fits in a few hundred lines because it doesn’t need to fight Python — it flows with it.
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
+ ```
40
45
 
41
46
  ---
42
47
 
43
- **Ato** is a lightweight Python library for experiment management in machine learning and data science.
44
- It provides flexible configuration management, experiment tracking, and hyperparameter optimization —
45
- all without the complexity or overhead of heavy frameworks.
48
+ ## Design Philosophy
46
49
 
47
- ## Why Ato?
50
+ Ato was built on three constraints:
48
51
 
49
- ### Core Differentiators
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.
50
55
 
51
- - **True Namespace Isolation**: MultiScope provides independent config contexts (unique to Ato!)
52
- - **Configuration Transparency**: Visualize exact config merge order - debug configs with `manual` command
53
- - **Built-in Experiment Tracking**: SQLite-based tracking with no external services required
54
- - **Structural Hashing**: Track experiment structure changes automatically
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.
55
58
 
56
- ### Developer Experience
59
+ **What Ato provides:**
60
+ - **Config composition** with explicit priority 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)
57
64
 
58
- - **Zero Boilerplate**: Auto-nested configs, lazy evaluation, attribute access
59
- - **CLI-first Design**: Configure experiments from command line without touching code
60
- - **Framework Agnostic**: Works with PyTorch, TensorFlow, JAX, or pure Python
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
61
70
 
62
- ## Quick Start
71
+ Ato is designed to work **between** tools, not replace them.
63
72
 
64
- ```bash
65
- pip install ato-python
66
- ```
73
+ ---
74
+
75
+ ## Quick Start
67
76
 
68
77
  ### 30-Second Example
69
78
 
@@ -73,14 +82,14 @@ from ato.scope import Scope
73
82
  scope = Scope()
74
83
 
75
84
  @scope.observe(default=True)
76
- def config(cfg):
77
- cfg.lr = 0.001
78
- cfg.batch_size = 32
79
- cfg.model = 'resnet50'
85
+ def config(config):
86
+ config.lr = 0.001
87
+ config.batch_size = 32
88
+ config.model = 'resnet50'
80
89
 
81
90
  @scope
82
- def train(cfg):
83
- print(f"Training {cfg.model} with lr={cfg.lr}")
91
+ def train(config):
92
+ print(f"Training {config.model} with lr={config.lr}")
84
93
  # Your training code here
85
94
 
86
95
  if __name__ == '__main__':
@@ -88,72 +97,60 @@ if __name__ == '__main__':
88
97
  # Override from CLI: python train.py lr=0.01 model=%resnet101%
89
98
  ```
90
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 (defaults → named configs → CLI → lazy evaluation)
105
+
91
106
  ---
92
107
 
93
108
  ## Table of Contents
94
109
 
95
110
  - [ADict: Enhanced Dictionary](#adict-enhanced-dictionary)
96
111
  - [Scope: Configuration Management](#scope-configuration-management)
97
- - [MultiScope: Namespace Isolation](#2-multiscope---multiple-configuration-contexts) ⭐ Unique to Ato
98
- - [Config Documentation & Debugging](#5-configuration-documentation--inspection) ⭐ Unique to Ato
112
+ - [MultiScope: Namespace Isolation](#multiscope-namespace-isolation)
113
+ - [Config Documentation & Debugging](#configuration-documentation--debugging)
99
114
  - [SQL Tracker: Experiment Tracking](#sql-tracker-experiment-tracking)
100
115
  - [Hyperparameter Optimization](#hyperparameter-optimization)
101
116
  - [Best Practices](#best-practices)
102
- - [Comparison with Hydra](#ato-vs-hydra)
117
+ - [Contributing](#contributing)
118
+ - [Composability](#composability)
103
119
 
104
120
  ---
105
121
 
106
122
  ## ADict: Enhanced Dictionary
107
123
 
108
- `ADict` is an enhanced dictionary designed for managing experiment configurations. It combines the simplicity of Python dictionaries with powerful features for ML workflows.
124
+ `ADict` is an enhanced dictionary for managing experiment configurations.
109
125
 
110
126
  ### Core Features
111
127
 
112
- These are the fundamental capabilities that make ADict powerful for experiment management:
113
-
114
128
  | Feature | Description | Why It Matters |
115
129
  |---------|-------------|----------------|
116
- | **Structural Hashing** | Hash based on keys + types, not values | Track when experiment structure changes |
130
+ | **Structural Hashing** | Hash based on keys + types, not values | Track when experiment **structure** changes (not just hyperparameters) |
117
131
  | **Nested Access** | Dot notation for nested configs | `config.model.lr` instead of `config['model']['lr']` |
118
132
  | **Format Agnostic** | Load/save JSON, YAML, TOML, XYZ | Work with any config format |
119
- | **Safe Updates** | `update_if_absent()` method | Prevent accidental overwrites |
133
+ | **Safe Updates** | `update_if_absent()` method | Merge configs without accidental overwrites |
134
+ | **Auto-nested** | `ADict.auto()` for lazy creation | `config.a.b.c = 1` just works - no KeyError |
120
135
 
121
- ### Developer Convenience Features
136
+ ### Examples
122
137
 
123
- These utilities maximize developer productivity and reduce boilerplate:
124
-
125
- | Feature | Description | Benefit |
126
- |---------|-------------|---------|
127
- | **Auto-nested (`ADict.auto()`)** | Infinite depth lazy creation | `config.a.b.c = 1` just works - no KeyError |
128
- | **Attribute-style Assignment** | `config.lr = 0.1` | Cleaner, more readable code |
129
- | **Conditional Updates** | Only update missing keys | Merge configs safely |
130
-
131
- ### Quick Examples
138
+ #### Structural Hashing
132
139
 
133
140
  ```python
134
141
  from ato.adict import ADict
135
142
 
136
- # Structural hashing - track config structure changes
143
+ # Same structure, different values
137
144
  config1 = ADict(lr=0.1, epochs=100, model='resnet50')
138
145
  config2 = ADict(lr=0.01, epochs=200, model='resnet101')
139
146
  print(config1.get_structural_hash() == config2.get_structural_hash()) # True
140
147
 
141
- config3 = ADict(lr=0.1, epochs='100', model='resnet50') # epochs is str!
148
+ # Different structure (epochs is str!)
149
+ config3 = ADict(lr=0.1, epochs='100', model='resnet50')
142
150
  print(config1.get_structural_hash() == config3.get_structural_hash()) # False
143
-
144
- # Load/save any format
145
- config = ADict.from_file('config.json')
146
- config.dump('config.yaml')
147
-
148
- # Safe updates
149
- config.update_if_absent(lr=0.01, scheduler='cosine') # Only adds scheduler
150
151
  ```
151
152
 
152
- ### Convenience Features in Detail
153
-
154
- #### Auto-nested: Zero Boilerplate Config Building
155
-
156
- The most loved feature - no more manual nesting:
153
+ #### Auto-nested Configs
157
154
 
158
155
  ```python
159
156
  # ❌ Traditional way
@@ -168,48 +165,24 @@ config.model.backbone.layers = [64, 128, 256] # Just works!
168
165
  config.data.augmentation.brightness = 0.2
169
166
  ```
170
167
 
171
- **Perfect for Scope integration**:
172
-
173
- ```python
174
- from ato.scope import Scope
175
-
176
- scope = Scope()
177
-
178
- @scope.observe(default=True)
179
- def config(cfg):
180
- # No pre-definition needed!
181
- cfg.training.optimizer.name = 'AdamW'
182
- cfg.training.optimizer.lr = 0.001
183
- cfg.model.encoder.num_layers = 12
184
- ```
185
-
186
- **Works with CLI**:
187
-
188
- ```bash
189
- python train.py model.backbone.resnet.depth=50 data.batch_size=32
190
- ```
191
-
192
- #### More Convenience Utilities
168
+ #### Format Agnostic
193
169
 
194
170
  ```python
195
- # Attribute-style access
196
- config.lr = 0.1
197
- print(config.lr) # Instead of config['lr']
198
-
199
- # Nested access
200
- print(config.model.backbone.type) # Clean and readable
171
+ # Load/save any format
172
+ config = ADict.from_file('config.json')
173
+ config.dump('config.yaml')
201
174
 
202
- # Conditional updates - merge configs safely
203
- base_config.update_if_absent(**experiment_config)
175
+ # Safe updates
176
+ config.update_if_absent(lr=0.01, scheduler='cosine') # Only adds scheduler
204
177
  ```
205
178
 
206
179
  ---
207
180
 
208
181
  ## Scope: Configuration Management
209
182
 
210
- Scope solves configuration complexity through **priority-based merging** and **CLI integration**. No more scattered config files or hard-coded parameters.
183
+ Scope manages configuration through **priority-based merging** and **CLI integration**.
211
184
 
212
- ### Key Concepts
185
+ ### Key Concept: Priority Chain
213
186
 
214
187
  ```
215
188
  Default Configs (priority=0)
@@ -249,17 +222,17 @@ if __name__ == '__main__':
249
222
 
250
223
  ```python
251
224
  @scope.observe(default=True) # Always applied
252
- def defaults(cfg):
253
- cfg.lr = 0.001
254
- cfg.epochs = 100
225
+ def defaults(config):
226
+ config.lr = 0.001
227
+ config.epochs = 100
255
228
 
256
229
  @scope.observe(priority=1) # Applied after defaults
257
- def high_lr(cfg):
258
- cfg.lr = 0.01
230
+ def high_lr(config):
231
+ config.lr = 0.01
259
232
 
260
233
  @scope.observe(priority=2) # Applied last
261
- def long_training(cfg):
262
- cfg.epochs = 300
234
+ def long_training(config):
235
+ config.epochs = 300
263
236
  ```
264
237
 
265
238
  ```bash
@@ -288,27 +261,25 @@ python train.py my_config lr=0.001 batch_size=128
288
261
 
289
262
  **Note**: Wrap strings with `%` (e.g., `%resnet101%`) instead of quotes.
290
263
 
291
- ### Advanced Features
292
-
293
- #### 1. Lazy Evaluation - Dynamic Configuration
264
+ ### Lazy Evaluation
294
265
 
295
266
  Sometimes you need configs that depend on other values set via CLI:
296
267
 
297
268
  ```python
298
269
  @scope.observe()
299
- def base_config(cfg):
300
- cfg.model = 'resnet50'
301
- cfg.dataset = 'imagenet'
270
+ def base_config(config):
271
+ config.model = 'resnet50'
272
+ config.dataset = 'imagenet'
302
273
 
303
274
  @scope.observe(lazy=True) # Evaluated AFTER CLI args
304
- def computed_config(cfg):
275
+ def computed_config(config):
305
276
  # Adjust based on dataset
306
- if cfg.dataset == 'imagenet':
307
- cfg.num_classes = 1000
308
- cfg.image_size = 224
309
- elif cfg.dataset == 'cifar10':
310
- cfg.num_classes = 10
311
- cfg.image_size = 32
277
+ if config.dataset == 'imagenet':
278
+ config.num_classes = 1000
279
+ config.image_size = 224
280
+ elif config.dataset == 'cifar10':
281
+ config.num_classes = 10
282
+ config.image_size = 32
312
283
  ```
313
284
 
314
285
  ```bash
@@ -320,29 +291,20 @@ python train.py dataset=%cifar10% computed_config
320
291
 
321
292
  ```python
322
293
  @scope.observe()
323
- def my_config(cfg):
324
- cfg.model = 'resnet50'
325
- cfg.num_layers = 50
294
+ def my_config(config):
295
+ config.model = 'resnet50'
296
+ config.num_layers = 50
326
297
 
327
298
  with Scope.lazy(): # Evaluated after CLI
328
- if cfg.model == 'resnet101':
329
- cfg.num_layers = 101
299
+ if config.model == 'resnet101':
300
+ config.num_layers = 101
330
301
  ```
331
302
 
332
- #### 2. MultiScope - Multiple Configuration Contexts
303
+ ### MultiScope: Namespace Isolation
333
304
 
334
- **Unique to Ato**: Manage completely separate configuration namespaces. Unlike Hydra's config groups, MultiScope provides true **namespace isolation** with independent priority systems.
305
+ Manage completely separate configuration namespaces with independent priority systems.
335
306
 
336
- ##### Why MultiScope?
337
-
338
- | Challenge | Hydra's Approach | Ato's MultiScope |
339
- |-----------|------------------|---------------------|
340
- | Separate model/data configs | Config groups in one namespace | **Independent scopes with own priorities** |
341
- | Avoid key collisions | Manual prefixing (`model.lr`, `train.lr`) | **Automatic namespace isolation** |
342
- | Different teams/modules | Single config file | **Each scope can be owned separately** |
343
- | Priority conflicts | Global priority system | **Per-scope priority system** |
344
-
345
- ##### Basic Usage
307
+ **Use case**: Different teams own different scopes without key collisions.
346
308
 
347
309
  ```python
348
310
  from ato.scope import Scope, MultiScope
@@ -354,84 +316,103 @@ scope = MultiScope(model_scope, data_scope)
354
316
  @model_scope.observe(default=True)
355
317
  def model_config(model):
356
318
  model.backbone = 'resnet50'
357
- model.pretrained = True
319
+ model.lr = 0.1 # Model-specific learning rate
358
320
 
359
321
  @data_scope.observe(default=True)
360
322
  def data_config(data):
361
323
  data.dataset = 'cifar10'
362
- data.batch_size = 32
324
+ data.lr = 0.001 # Data augmentation learning rate (no conflict!)
363
325
 
364
326
  @scope
365
327
  def train(model, data): # Named parameters match scope names
366
- print(f"Training {model.backbone} on {data.dataset}")
328
+ # Both have 'lr' but in separate namespaces!
329
+ print(f"Model LR: {model.lr}, Data LR: {data.lr}")
367
330
  ```
368
331
 
369
- ##### Real-world: Team Collaboration
332
+ **Key advantage**: `model.lr` and `data.lr` are completely independent. No need for naming conventions like `model_lr` vs `data_lr`.
370
333
 
371
- Different team members can own different scopes without conflicts:
334
+ **CLI with MultiScope:**
372
335
 
373
- ```python
374
- # team_model.py - ML team owns this
375
- model_scope = Scope(name='model')
336
+ ```bash
337
+ # Override model scope only
338
+ python train.py model.backbone=%resnet101%
376
339
 
377
- @model_scope.observe(default=True)
378
- def resnet_default(model):
379
- model.backbone = 'resnet50'
380
- model.lr = 0.1 # Model-specific learning rate
340
+ # Override data scope only
341
+ python train.py data.dataset=%imagenet%
381
342
 
382
- @model_scope.observe(priority=1)
383
- def resnet101(model):
384
- model.backbone = 'resnet101'
385
- model.lr = 0.05 # Different lr for bigger model
343
+ # Override both
344
+ python train.py model.backbone=%resnet101% data.dataset=%imagenet%
345
+ ```
386
346
 
387
- # team_data.py - Data team owns this
388
- data_scope = Scope(name='data')
347
+ ### Configuration Documentation & Debugging
389
348
 
390
- @data_scope.observe(default=True)
391
- def cifar_default(data):
392
- data.dataset = 'cifar10'
393
- data.lr = 0.001 # Data augmentation learning rate (no conflict!)
349
+ **The `manual` command** visualizes the exact order of configuration application.
394
350
 
395
- @data_scope.observe(priority=1)
396
- def imagenet(data):
397
- data.dataset = 'imagenet'
398
- data.workers = 16
351
+ ```python
352
+ @scope.observe(default=True)
353
+ def config(config):
354
+ config.lr = 0.001
355
+ config.batch_size = 32
356
+ config.model = 'resnet50'
399
357
 
400
- # train.py - Integration point
401
- from team_model import model_scope
402
- from team_data import data_scope
358
+ @scope.manual
359
+ def config_docs(config):
360
+ config.lr = 'Learning rate for optimizer'
361
+ config.batch_size = 'Number of samples per batch'
362
+ config.model = 'Model architecture (resnet50, resnet101, etc.)'
363
+ ```
403
364
 
404
- scope = MultiScope(model_scope, data_scope)
365
+ ```bash
366
+ python train.py manual
367
+ ```
405
368
 
406
- @scope
407
- def train(model, data):
408
- # Both have 'lr' but in separate namespaces!
409
- print(f"Model LR: {model.lr}, Data LR: {data.lr}")
369
+ **Output:**
410
370
  ```
371
+ --------------------------------------------------
372
+ [Scope "config"]
373
+ (The Applying Order of Views)
374
+ config → (CLI Inputs)
411
375
 
412
- **Key advantage**: `model.lr` and `data.lr` are completely independent. No need for naming conventions like `model_lr` vs `data_lr`.
376
+ (User Manuals)
377
+ lr: Learning rate for optimizer
378
+ batch_size: Number of samples per batch
379
+ model: Model architecture (resnet50, resnet101, etc.)
380
+ --------------------------------------------------
381
+ ```
413
382
 
414
- ##### CLI with MultiScope
383
+ **Why this matters:**
384
+ When debugging "why is this config value not what I expect?", you can see **exactly** which function set it and in what order.
415
385
 
416
- Override each scope independently:
386
+ **Complex example:**
417
387
 
418
- ```bash
419
- # Override model scope only
420
- python train.py model.backbone=%resnet101%
388
+ ```python
389
+ @scope.observe(default=True)
390
+ def defaults(config):
391
+ config.lr = 0.001
421
392
 
422
- # Override data scope only
423
- python train.py data.dataset=%imagenet%
393
+ @scope.observe(priority=1)
394
+ def experiment_config(config):
395
+ config.lr = 0.01
424
396
 
425
- # Override both
426
- python train.py model.backbone=%resnet101% data.dataset=%imagenet%
397
+ @scope.observe(priority=2)
398
+ def another_config(config):
399
+ config.lr = 0.1
427
400
 
428
- # Call named configs per scope
429
- python train.py resnet101 imagenet
401
+ @scope.observe(lazy=True)
402
+ def adaptive_lr(config):
403
+ if config.batch_size > 64:
404
+ config.lr = config.lr * 2
430
405
  ```
431
406
 
432
- #### 3. Import/Export Configs
407
+ When you run `python train.py manual`, you see:
408
+ ```
409
+ (The Applying Order of Views)
410
+ defaults → experiment_config → another_config → (CLI Inputs) → adaptive_lr
411
+ ```
433
412
 
434
- Ato supports importing configs from multiple frameworks:
413
+ Now it's **crystal clear** why `lr=0.1` (from `another_config`) and not `0.01`!
414
+
415
+ ### Config Import/Export
435
416
 
436
417
  ```python
437
418
  @scope.observe()
@@ -442,36 +423,26 @@ def load_external(config):
442
423
 
443
424
  # Export to any format
444
425
  config.dump('output/final_config.toml')
445
-
446
- # Import OpenMMLab configs - handles _base_ inheritance automatically
447
- config.load_mm_config('mmdet_configs/faster_rcnn.py')
448
426
  ```
449
427
 
450
- **OpenMMLab compatibility** is built-in:
451
- - Automatically resolves `_base_` inheritance chains
452
- - Supports `_delete_` keys for config overriding
453
- - Makes migration from MMDetection/MMSegmentation/etc. seamless
428
+ **OpenMMLab compatibility:**
454
429
 
455
- **Hydra-style config composition** is also built-in via `compose_hierarchy`:
430
+ ```python
431
+ # Import OpenMMLab configs - handles _base_ inheritance automatically
432
+ config.load_mm_config('mmdet_configs/faster_rcnn.py')
433
+ ```
434
+
435
+ **Hierarchical composition:**
456
436
 
457
437
  ```python
458
438
  from ato.adict import ADict
459
439
 
460
- # Hydra-style directory structure:
461
- # configs/
462
- # ├── config.yaml # base config
463
- # ├── model/
464
- # │ ├── resnet50.yaml
465
- # │ └── resnet101.yaml
466
- # └── data/
467
- # ├── cifar10.yaml
468
- # └── imagenet.yaml
469
-
440
+ # Load configs from directory structure
470
441
  config = ADict.compose_hierarchy(
471
442
  root='configs',
472
443
  config_filename='config',
473
444
  select={
474
- 'model': 'resnet50', # or ['resnet50', 'resnet101'] for multiple
445
+ 'model': 'resnet50',
475
446
  'data': 'imagenet'
476
447
  },
477
448
  overrides={
@@ -483,16 +454,7 @@ config = ADict.compose_hierarchy(
483
454
  )
484
455
  ```
485
456
 
486
- **Key features**:
487
- - Config groups (model/, data/, optimizer/, etc.)
488
- - Automatic file discovery (tries .yaml, .json, .toml, .xyz)
489
- - Dotted overrides (`model.lr=0.01`)
490
- - Required key validation
491
- - Flexible error handling
492
-
493
- #### 4. Argparse Integration
494
-
495
- Mix Ato with existing argparse code:
457
+ ### Argparse Integration
496
458
 
497
459
  ```python
498
460
  from ato.scope import Scope
@@ -504,170 +466,24 @@ parser.add_argument('--gpu', type=int, default=0)
504
466
  parser.add_argument('--seed', type=int, default=42)
505
467
 
506
468
  @scope.observe(default=True)
507
- def config(cfg):
508
- cfg.lr = 0.001
509
- cfg.batch_size = 32
469
+ def config(config):
470
+ config.lr = 0.001
471
+ config.batch_size = 32
510
472
 
511
473
  @scope
512
- def train(cfg):
513
- print(f"GPU: {cfg.gpu}, LR: {cfg.lr}")
474
+ def train(config):
475
+ print(f"GPU: {config.gpu}, LR: {config.lr}")
514
476
 
515
477
  if __name__ == '__main__':
516
478
  parser.parse_args() # Merges argparse with scope
517
479
  train()
518
480
  ```
519
481
 
520
- #### 5. Configuration Documentation & Inspection
521
-
522
- **One of Ato's most powerful features**: Auto-generate documentation AND visualize the exact order of configuration application.
523
-
524
- ##### Basic Documentation
525
-
526
- ```python
527
- @scope.manual
528
- def config_docs(cfg):
529
- cfg.lr = 'Learning rate for optimizer'
530
- cfg.batch_size = 'Number of samples per batch'
531
- cfg.model = 'Model architecture (resnet50, resnet101, etc.)'
532
- ```
533
-
534
- ```bash
535
- python train.py manual
536
- ```
537
-
538
- **Output:**
539
- ```
540
- --------------------------------------------------
541
- [Scope "config"]
542
- (The Applying Order of Views)
543
- defaults → (CLI Inputs) → lazy_config → main
544
-
545
- (User Manuals)
546
- config.lr: Learning rate for optimizer
547
- config.batch_size: Number of samples per batch
548
- config.model: Model architecture (resnet50, resnet101, etc.)
549
- --------------------------------------------------
550
- ```
551
-
552
- ##### Why This Matters
553
-
554
- The **applying order visualization** shows you **exactly** how your configs are merged:
555
- - Which config functions are applied (in order)
556
- - When CLI inputs override values
557
- - Where lazy configs are evaluated
558
- - The final function that uses the config
559
-
560
- **This prevents configuration bugs** by making the merge order explicit and debuggable.
561
-
562
- ##### MultiScope Documentation
563
-
564
- For complex projects with multiple scopes, `manual` shows each scope separately:
565
-
566
- ```python
567
- from ato.scope import Scope, MultiScope
568
-
569
- model_scope = Scope(name='model')
570
- train_scope = Scope(name='train')
571
- scope = MultiScope(model_scope, train_scope)
572
-
573
- @model_scope.observe(default=True)
574
- def model_defaults(model):
575
- model.backbone = 'resnet50'
576
- model.num_layers = 50
577
-
578
- @model_scope.observe(priority=1)
579
- def model_advanced(model):
580
- model.pretrained = True
581
-
582
- @model_scope.observe(lazy=True)
583
- def model_lazy(model):
584
- if model.backbone == 'resnet101':
585
- model.num_layers = 101
586
-
587
- @train_scope.observe(default=True)
588
- def train_defaults(train):
589
- train.lr = 0.001
590
- train.epochs = 100
591
-
592
- @model_scope.manual
593
- def model_docs(model):
594
- model.backbone = 'Model backbone architecture'
595
- model.num_layers = 'Number of layers in the model'
596
-
597
- @train_scope.manual
598
- def train_docs(train):
599
- train.lr = 'Learning rate for optimizer'
600
- train.epochs = 'Total training epochs'
601
-
602
- @scope
603
- def main(model, train):
604
- print(f"Training {model.backbone} with lr={train.lr}")
605
-
606
- if __name__ == '__main__':
607
- main()
608
- ```
609
-
610
- ```bash
611
- python train.py manual
612
- ```
613
-
614
- **Output:**
615
- ```
616
- --------------------------------------------------
617
- [Scope "model"]
618
- (The Applying Order of Views)
619
- model_defaults → model_advanced → (CLI Inputs) → model_lazy → main
620
-
621
- (User Manuals)
622
- model.backbone: Model backbone architecture
623
- model.num_layers: Number of layers in the model
624
- --------------------------------------------------
625
- [Scope "train"]
626
- (The Applying Order of Views)
627
- train_defaults → (CLI Inputs) → main
628
-
629
- (User Manuals)
630
- train.lr: Learning rate for optimizer
631
- train.epochs: Total training epochs
632
- --------------------------------------------------
633
- ```
634
-
635
- ##### Real-world Example
636
-
637
- This is especially valuable when debugging why a config value isn't what you expect:
638
-
639
- ```python
640
- @scope.observe(default=True)
641
- def defaults(cfg):
642
- cfg.lr = 0.001
643
-
644
- @scope.observe(priority=1)
645
- def experiment_config(cfg):
646
- cfg.lr = 0.01
647
-
648
- @scope.observe(priority=2)
649
- def another_config(cfg):
650
- cfg.lr = 0.1
651
-
652
- @scope.observe(lazy=True)
653
- def adaptive_lr(cfg):
654
- if cfg.batch_size > 64:
655
- cfg.lr = cfg.lr * 2
656
- ```
657
-
658
- When you run `python train.py manual`, you see:
659
- ```
660
- (The Applying Order of Views)
661
- defaults → experiment_config → another_config → (CLI Inputs) → adaptive_lr → main
662
- ```
663
-
664
- Now it's **crystal clear** why `lr=0.1` (from `another_config`) and not `0.01`!
665
-
666
482
  ---
667
483
 
668
484
  ## SQL Tracker: Experiment Tracking
669
485
 
670
- Lightweight experiment tracking using SQLite - no external services, no setup complexity.
486
+ Lightweight experiment tracking using SQLite.
671
487
 
672
488
  ### Why SQL Tracker?
673
489
 
@@ -675,6 +491,7 @@ Lightweight experiment tracking using SQLite - no external services, no setup co
675
491
  - **Full History**: Track all runs, metrics, and artifacts
676
492
  - **Smart Search**: Find similar experiments by config structure
677
493
  - **Code Versioning**: Track code changes via fingerprints
494
+ - **Offline-first**: No network required, sync to cloud tracking later if needed
678
495
 
679
496
  ### Database Schema
680
497
 
@@ -690,7 +507,7 @@ Project (my_ml_project)
690
507
  └── ...
691
508
  ```
692
509
 
693
- ### Quick Start
510
+ ### Usage
694
511
 
695
512
  #### Logging Experiments
696
513
 
@@ -764,22 +581,7 @@ stats = finder.get_trace_statistics('image_classification', trace_id='model_forw
764
581
  print(f"Model forward pass has {stats['static_trace_versions']} versions")
765
582
  ```
766
583
 
767
- ### Real-world Example: Experiment Comparison
768
-
769
- ```python
770
- # Compare hyperparameter impact
771
- finder = SQLFinder(config)
772
-
773
- runs = finder.get_runs_in_project('my_project')
774
- for run in runs:
775
- # Get final accuracy
776
- final_metrics = [m for m in run.metrics if m.key == 'val_accuracy']
777
- best_acc = max(m.value for m in final_metrics) if final_metrics else 0
778
-
779
- print(f"LR: {run.config.lr}, Batch: {run.config.batch_size} → Acc: {best_acc:.2%}")
780
- ```
781
-
782
- ### Features Summary
584
+ ### Features
783
585
 
784
586
  | Feature | Description |
785
587
  |---------|-------------|
@@ -795,38 +597,6 @@ for run in runs:
795
597
 
796
598
  Built-in **Hyperband** algorithm for efficient hyperparameter search with early stopping.
797
599
 
798
- ### Extensible Design
799
-
800
- Ato's hyperopt module is built for extensibility and reusability:
801
-
802
- | Component | Purpose | Benefit |
803
- |-----------|---------|---------|
804
- | `GridSpaceMixIn` | Parameter sampling logic | Reusable across different algorithms |
805
- | `HyperOpt` | Base optimization class | Easy to implement custom strategies |
806
- | `DistributedMixIn` | Distributed training support | Optional, composable |
807
-
808
- **This design makes it trivial to implement custom search algorithms**:
809
-
810
- ```python
811
- from ato.hyperopt.base import GridSpaceMixIn, HyperOpt
812
-
813
- class RandomSearch(GridSpaceMixIn, HyperOpt):
814
- def main(self, func):
815
- # Reuse GridSpaceMixIn.prepare_distributions()
816
- configs = self.prepare_distributions(self.config, self.search_spaces)
817
-
818
- # Implement random sampling
819
- import random
820
- random.shuffle(configs)
821
-
822
- results = []
823
- for config in configs[:10]: # Sample 10 random configs
824
- metric = func(config)
825
- results.append((config, metric))
826
-
827
- return max(results, key=lambda x: x[1])
828
- ```
829
-
830
600
  ### How Hyperband Works
831
601
 
832
602
  Hyperband uses successive halving:
@@ -895,8 +665,6 @@ if __name__ == '__main__':
895
665
 
896
666
  ### Automatic Step Calculation
897
667
 
898
- Let Hyperband compute optimal training steps:
899
-
900
668
  ```python
901
669
  hyperband = HyperBand(scope, search_spaces, halving_rate=0.3, num_min_samples=4)
902
670
 
@@ -926,9 +694,7 @@ Space types:
926
694
  - `LOG`: Logarithmic spacing (good for learning rates)
927
695
  - `LINEAR`: Linear spacing (default)
928
696
 
929
- ### Distributed Hyperparameter Search
930
-
931
- Ato supports distributed hyperparameter optimization out of the box:
697
+ ### Distributed Search
932
698
 
933
699
  ```python
934
700
  from ato.hyperopt.hyperband import DistributedHyperBand
@@ -965,11 +731,37 @@ if __name__ == '__main__':
965
731
  print(f"Best config: {result.config}")
966
732
  ```
967
733
 
968
- **Key features**:
969
- - Automatic work distribution across GPUs
970
- - Synchronized config selection via `broadcast_object_from_root`
971
- - Results aggregation with `all_gather_object`
972
- - Compatible with PyTorch DDP, FSDP, DeepSpeed
734
+ ### Extensible Design
735
+
736
+ Ato's hyperopt module is built for extensibility:
737
+
738
+ | Component | Purpose |
739
+ |-----------|---------|
740
+ | `GridSpaceMixIn` | Parameter sampling logic (reusable) |
741
+ | `HyperOpt` | Base optimization class |
742
+ | `DistributedMixIn` | Distributed training support (optional) |
743
+
744
+ **Example: Implement custom search algorithm**
745
+
746
+ ```python
747
+ from ato.hyperopt.base import GridSpaceMixIn, HyperOpt
748
+
749
+ class RandomSearch(GridSpaceMixIn, HyperOpt):
750
+ def main(self, func):
751
+ # Reuse GridSpaceMixIn.prepare_distributions()
752
+ configs = self.prepare_distributions(self.config, self.search_spaces)
753
+
754
+ # Implement random sampling
755
+ import random
756
+ random.shuffle(configs)
757
+
758
+ results = []
759
+ for config in configs[:10]: # Sample 10 random configs
760
+ metric = func(config)
761
+ results.append((config, metric))
762
+
763
+ return max(results, key=lambda x: x[1])
764
+ ```
973
765
 
974
766
  ---
975
767
 
@@ -997,33 +789,34 @@ my_project/
997
789
  ```python
998
790
  # configs/default.py
999
791
  from ato.scope import Scope
792
+ from ato.adict import ADict
1000
793
 
1001
794
  scope = Scope()
1002
795
 
1003
796
  @scope.observe(default=True)
1004
- def defaults(cfg):
797
+ def defaults(config):
1005
798
  # Data
1006
- cfg.data = ADict(
799
+ config.data = ADict(
1007
800
  dataset='cifar10',
1008
801
  batch_size=32,
1009
802
  num_workers=4
1010
803
  )
1011
804
 
1012
805
  # Model
1013
- cfg.model = ADict(
806
+ config.model = ADict(
1014
807
  backbone='resnet50',
1015
808
  pretrained=True
1016
809
  )
1017
810
 
1018
811
  # Training
1019
- cfg.train = ADict(
812
+ config.train = ADict(
1020
813
  lr=0.001,
1021
814
  epochs=100,
1022
815
  optimizer='adam'
1023
816
  )
1024
817
 
1025
818
  # Experiment tracking
1026
- cfg.experiment = ADict(
819
+ config.experiment = ADict(
1027
820
  project_name='my_project',
1028
821
  sql=ADict(db_path='sqlite:///experiments.db')
1029
822
  )
@@ -1037,14 +830,14 @@ from ato.db_routers.sql.manager import SQLLogger
1037
830
  from configs.default import scope
1038
831
 
1039
832
  @scope
1040
- def train(cfg):
833
+ def train(config):
1041
834
  # Setup experiment tracking
1042
- logger = SQLLogger(cfg)
1043
- run_id = logger.run(tags=[cfg.model.backbone, cfg.data.dataset])
835
+ logger = SQLLogger(config)
836
+ run_id = logger.run(tags=[config.model.backbone, config.data.dataset])
1044
837
 
1045
838
  try:
1046
839
  # Training loop
1047
- for epoch in range(cfg.train.epochs):
840
+ for epoch in range(config.train.epochs):
1048
841
  loss = train_epoch()
1049
842
  acc = validate()
1050
843
 
@@ -1082,12 +875,6 @@ See `pyproject.toml` for full dependencies.
1082
875
 
1083
876
  ---
1084
877
 
1085
- ## License
1086
-
1087
- MIT License
1088
-
1089
- ---
1090
-
1091
878
  ## Contributing
1092
879
 
1093
880
  Contributions are welcome! Please feel free to submit issues or pull requests.
@@ -1100,82 +887,92 @@ cd ato
1100
887
  pip install -e .
1101
888
  ```
1102
889
 
890
+ ### Quality Assurance
891
+
892
+ Ato's design philosophy — **structural neutrality** and **debuggable composition** — extends to our testing practices.
893
+
894
+ **Release Policy:**
895
+ - **All 100+ unit tests must pass before any release**
896
+ - No exceptions, no workarounds
897
+ - Tests cover every module: ADict, Scope, MultiScope, SQLTracker, HyperBand
898
+
899
+ **Why this matters:**
900
+ 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.
901
+
902
+ Run tests locally:
903
+ ```bash
904
+ python -m pytest unit_tests/
905
+ ```
906
+
1103
907
  ---
1104
908
 
1105
- ## Comparison with Other Tools
1106
-
1107
- | Feature | Ato | MLflow | W&B | Hydra |
1108
- |---------|--------|--------|-----|-------|
1109
- | **Core Features** |
1110
- | Zero setup | ✅ | ❌ | ❌ | ✅ |
1111
- | Offline-first | ✅ | Partial | ❌ | ✅ |
1112
- | Config priority system | ✅ Explicit | Partial (Tags) | Partial (Run params) | ✅ Override |
1113
- | **True namespace isolation** | **✅ MultiScope** | **❌** | **❌** | **❌ Config groups only** |
1114
- | **Config merge visualization** | **✅ `manual`** | **❌** | **❌** | **Partial (`--cfg` tree)** |
1115
- | Structural hashing | ✅ | ❌ | ❌ | ❌ |
1116
- | Built-in HyperOpt | ✅ Hyperband | ❌ | ✅ Sweeps | Plugins (Optuna) |
1117
- | CLI-first design | | | ❌ | ✅ |
1118
- | **Compatibility** |
1119
- | Framework agnostic | | | ✅ | ✅ |
1120
- | Distributed training | ✅ Native + DDP/FSDP⁽¹⁾ | ✅ | ✅ | ✅ |
1121
- | Distributed HyperOpt | ✅ `DistributedHyperBand` | ❌ | Partial | Plugins |
1122
- | Hydra-style composition | ✅ `compose_hierarchy` | N/A | N/A | Native |
1123
- | OpenMMLab configs | `load_mm_config` | | | |
1124
- | **Visualization & UI** |
1125
- | Web dashboard | 🔜 Planned | ✅ | ✅ | ❌ |
1126
- | Real-time metrics | 🔜 Planned | ✅ | ✅ | ❌ |
1127
- | Interactive plots | 🔜 Planned | | ✅ | ❌ |
1128
- | Metric comparison UI | 🔜 Planned | | ✅ | ❌ |
1129
- | **Advanced Features** |
1130
- | Model registry | 🔜 Planned | ✅ | ✅ | ❌ |
1131
- | Dataset versioning | 🔜 Planned | Partial | ✅ | ❌ |
1132
- | Team collaboration | ✅ MultiScope⁽²⁾ | ✅ Platform | ✅ Platform | ❌ |
1133
-
1134
- ⁽¹⁾ Native distributed hyperparameter optimization via `DistributedHyperBand`. Regular training is compatible with any distributed framework (DDP, FSDP, DeepSpeed) - just integrate logging, no special code needed.
1135
-
1136
- ⁽²⁾ Team collaboration via MultiScope: separate config ownership per team (e.g., Team A owns model scope, Team B owns data scope) without naming conflicts.
1137
-
1138
- **Note on config compatibility**: Ato provides built-in support for other config frameworks:
1139
- - **Hydra-style composition**: `compose_hierarchy()` supports config groups, select, overrides - full compatibility
1140
- - **OpenMMLab configs**: `load_mm_config()` handles `_base_` inheritance and `_delete_` keys
1141
- - Migration from existing projects is seamless - just import your configs and go
1142
-
1143
- ### Ato vs. Hydra
1144
-
1145
- While Hydra is excellent for config composition, Ato provides unique features:
1146
-
1147
- | Aspect | Hydra | Ato |
1148
- |--------|-------|--------|
1149
- | **Namespace isolation** | Config groups share namespace | ✅ MultiScope with independent namespaces<br/>(no key collisions) |
1150
- | **Priority system** | Single global override system | Per-scope priority + lazy evaluation |
1151
- | **Config merge debugging** | Tree view (`--cfg`)<br/>Shows final config | ✅ `manual` command<br/>Shows merge order & execution flow |
1152
- | **Experiment tracking** | Requires external tools<br/>(MLflow/W&B) | ✅ Built-in SQL tracker |
1153
- | **Team workflow** | Single config file ownership | ✅ Separate scope ownership per team⁽³⁾ |
1154
-
1155
- ⁽³⁾ Example: Team A defines `model_scope`, Team B defines `data_scope`, both can use `model.lr` and `data.lr` without conflicts.
1156
-
1157
- **Use Ato over Hydra when:**
1158
- - Multiple teams need independent config ownership (MultiScope)
1159
- - You want to avoid key collision issues (no manual prefixing needed)
1160
- - You need to debug why a config value was set (`manual` command)
1161
- - You want experiment tracking without adding MLflow/W&B
1162
- - You're migrating from OpenMMLab projects
1163
-
1164
- **Use Hydra when:**
1165
- - You have very deep config hierarchies with complex inheritance
1166
- - You prefer YAML over Python
1167
- - You need the mature plugin ecosystem (Ray, Joblib, etc.)
1168
- - You don't need namespace isolation
1169
-
1170
- **Why not both?**
1171
- - Ato has **built-in Hydra-style composition** via `compose_hierarchy()`
1172
- - You can use Hydra's directory structure and config groups directly in Ato
1173
- - Get MultiScope + experiment tracking + merge debugging on top of Hydra's composition
1174
- - Migration is literally just replacing `hydra.compose()` with `ADict.compose_hierarchy()`
1175
-
1176
- **Ato is for you if:**
1177
- - You want lightweight, offline-first experiment tracking
1178
- - You need **true namespace isolation for team collaboration**
1179
- - **You want to debug config merge order visually** (unique to Ato!)
1180
- - You prefer simple Python over complex frameworks
1181
- - You want reproducibility without overhead
909
+ ## Composability
910
+
911
+ Ato is designed to **compose** with existing tools, not replace them.
912
+
913
+ ### Works Where Other Systems Require Ecosystems
914
+
915
+ **Config composition:**
916
+ - Import OpenMMLab configs: `config.load_mm_config('mmdet_configs/faster_rcnn.py')`
917
+ - Load Hydra-style hierarchies: `ADict.compose_hierarchy(root='configs', select={'model': 'resnet50'})`
918
+ - Mix with argparse: `Scope(use_external_parser=True)`
919
+
920
+ **Experiment tracking:**
921
+ - Track locally in SQLite (zero setup)
922
+ - Sync to MLflow/W&B when you need dashboards
923
+ - Or use both: local SQLite + cloud tracking
924
+
925
+ **Hyperparameter optimization:**
926
+ - Built-in Hyperband
927
+ - Or compose with Optuna/Ray Tune Ato's configs work with any optimizer
928
+
929
+ ### Three Capabilities Other Tools Don't Provide
930
+
931
+ 1. **MultiScope** True namespace isolation with independent priority systems
932
+ 2. **`manual` command** Visualize exact config merge order for debugging
933
+ 3. **Structural hashing** — Track when experiment **architecture** changes, not just values
934
+
935
+ ### When to Use Ato
936
+
937
+ **Use Ato when:**
938
+ - You want zero boilerplate config management
939
+ - You need to debug why a config value isn't what you expect
940
+ - You're working on multi-team projects with namespace conflicts
941
+ - You want local-first experiment tracking
942
+ - You're migrating between config/tracking systems
943
+
944
+ **Ato works alongside:**
945
+ - Hydra (config composition)
946
+ - MLflow/W&B (cloud tracking)
947
+ - Optuna/Ray Tune (advanced hyperparameter search)
948
+ - PyTorch/TensorFlow/JAX (any ML framework)
949
+
950
+ ---
951
+
952
+ ## Roadmap
953
+
954
+ Ato's design constraint is **structural neutrality** adding capabilities without creating dependencies.
955
+
956
+ ### Planned: Local Dashboard (Optional Module)
957
+
958
+ A lightweight HTML dashboard for teams that want visual exploration without committing to cloud platforms:
959
+
960
+ **What it adds:**
961
+ - Metric comparison & trends (read-only view of SQLite data)
962
+ - Run history & artifact browsing
963
+ - Config diff visualization
964
+ - Interactive hyperparameter analysis
965
+
966
+ **Design constraints:**
967
+ - No hard dependency — Ato core works 100% without the dashboard
968
+ - Separate process — doesn't block or modify runs
969
+ - Zero lock-in delete it anytime, training code doesn't change
970
+ - Composable use alongside MLflow/W&B
971
+
972
+ **Guiding principle:** Ato remains a set of **independent, composable tools** — not a platform you commit to.
973
+
974
+ ---
975
+
976
+ ## License
977
+
978
+ MIT License