The piter operator provides a lightweight, composable way to create parameter sweeps using simple dictionaries and operators. Unlike the Sweep class which requires @proto decorated classes, piter works with plain dictionaries and supports lazy evaluation for memory efficiency.
Syntax: piter @ {...}
params-proto uses the @ operator for clean, readable parameter iteration:
from params_proto.hyper import piter# Create a parameter sweep from a dictionary (zips by default)configs = piter @ {"lr": [0.001, 0.01], "batch_size": [32, 64]}# Iterate over zipped configurations (2 configs)for config in configs: print(config) # {'lr': 0.001, 'batch_size': 32} # {'lr': 0.01, 'batch_size': 64}# For Cartesian product, use the * operator (only first needs piter @)configs = piter @ {"lr": [0.001, 0.01]} * {"batch_size": [32, 64]}# This creates 4 configs: all combinations
Key Features
Lazy evaluation: Configurations are generated on-the-fly, not stored in memory
Composable: Combine iterators using operators (*, %, **)
Reusable: Results are cached, so you can iterate multiple times
Memory efficient: Only materializes when needed via .to_list() or len()
Basic Usage
Creating a piter
python
# Lists of values are zipped element-wise (default behavior)configs = piter @ { "lr": [0.001, 0.01, 0.1], "batch_size": [32, 64, 128]}# Produces 3 configs (zipped): (0.001, 32), (0.01, 64), (0.1, 128)# Single valuesfixed = piter @ {"seed": 42, "epochs": 100}# Produces 1 config# For Cartesian product, use * operator (only first needs piter @)configs = piter @ {"lr": [0.001, 0.01, 0.1]} * {"batch_size": [32, 64]}# Produces 6 configs (3 × 2)# With prefixes for multiple parameter groups (zipped)configs = piter @ { "model.depth": [18, 50], "training.lr": [0.001, 0.01]}# Produces 2 configs (zipped)
Materializing Configs
python
# Lazy iteration (recommended)for config in configs: train(config)# Convert to list (materializes all configs)config_list = configs.to_list()# orconfig_list = configs.list# Get length (materializes internally)num_configs = len(configs)
Operators
Cartesian Product (*)
Combine parameter iterators to create all possible combinations. When chaining multiple dicts, only the first needs piter @:
python
# Preferred: chain with * operator (only first needs piter @)combined = piter @ {"lr": [0.001, 0.01]} * {"batch_size": [32, 64]}list(combined)# [# {'lr': 0.001, 'batch_size': 32},# {'lr': 0.001, 'batch_size': 64},# {'lr': 0.01, 'batch_size': 32},# {'lr': 0.01, 'batch_size': 64}# ]# Also works: separate piter @ for each (legacy style)piter1 = piter @ {"lr": [0.001, 0.01]}piter2 = piter @ {"batch_size": [32, 64]}combined = piter1 * piter2 # Same result
Use case: Exploring all combinations of independent hyperparameters.
Override (%)
Apply fixed parameters to all configurations:
python
# Create configs with Cartesian productconfigs = piter @ {"lr": [0.001, 0.01]} * {"batch_size": [32, 64]}# Override with a dictwith_seed = configs % {"seed": 42, "device": "cuda"}list(with_seed)# [# {'lr': 0.001, 'batch_size': 32, 'seed': 42, 'device': 'cuda'},# {'lr': 0.001, 'batch_size': 64, 'seed': 42, 'device': 'cuda'},# {'lr': 0.01, 'batch_size': 32, 'seed': 42, 'device': 'cuda'},# {'lr': 0.01, 'batch_size': 64, 'seed': 42, 'device': 'cuda'}# ]# Override with another piter (uses first config)with_defaults = configs % (piter @ {"seed": 42, "device": "cuda"})
Use case: Adding fixed parameters (seed, device, logging config) to all experiments.
# Good: Iterate lazilyfor config in experiments: train(config)# Avoid: Unnecessary materializationall_configs = experiments.to_list() # Uses memoryfor config in all_configs: train(config)
3. Use operators for clarity
python
# Good: Use * for Cartesian product (only first needs piter @)grid = piter @ {"lr": [0.001, 0.01]} * {"batch_size": [32, 64]}# 4 configs: all combinations# Good: Use zip (default) for related parameterspaired = piter @ {"lr": [0.001, 0.01], "weight_decay": [0.0001, 0.001]}# 2 configs: (0.001, 0.0001) and (0.01, 0.001)# Good: Use % for fixed valueswith_defaults = (piter @ {"lr": [0.001, 0.01]}) % {"seed": 42, "device": "cuda"}# Avoid: Mixing independent parameters in single dict (implicit zip)mixed = piter @ {"lr": [0.001, 0.01], "batch_size": [32, 64]}# Only 2 configs (zipped), might not be what you want for grid search
4. Combine with type-safe configs in production
python
from params_proto import protofrom params_proto.hyper import piter@protoclass Config: lr: float = 0.001 batch_size: int = 32 seed: int = 42# Use piter for sweep definition (Cartesian product for grid search)sweep_configs = ( piter @ {"lr": [0.001, 0.01, 0.1]} * {"batch_size": [32, 64]}) % {"seed": 42}# Apply to typed configfor overrides in sweep_configs: Config._update(overrides) train() # Config.lr, Config.batch_size are type-checked
Advanced Examples
Conditional Parameter Sweeps
python
# Different learning rates for different optimizersadam_configs = piter @ {"optimizer": "adam"} * {"lr": [0.0001, 0.001, 0.01]}sgd_configs = ( piter @ {"optimizer": "sgd"} * {"lr": [0.01, 0.1, 1.0]} * {"momentum": [0.9, 0.95]})# Combine into single sweep (use list concatenation)all_configs = adam_configs.to_list() + sgd_configs.to_list()# 3 adam configs + 6 sgd configs = 9 total
Nested Grids with Fixed Outer Parameters
python
# Coarse gridcoarse = piter @ {"lr": [0.001, 0.01, 0.1]}# For each coarse lr, fine-tune batch sizefine_tuned = []for coarse_config in coarse: fine = (piter @ {"batch_size": [16, 32, 64, 128]}) % coarse_config fine_tuned.extend(fine.to_list())# 12 total configs (3 lr × 4 batch_size)
Hierarchical Parameter Groups
python
# Dataset variationsdatasets = piter @ {"data.name": ["cifar10", "cifar100", "imagenet"]}# Model architectures per datasetcifar_models = piter @ {"model.type": ["resnet18", "resnet34"]}imagenet_models = piter @ {"model.type": ["resnet50", "resnet101"]}# Training configstraining = piter @ {"training.lr": [0.001, 0.01]}# Compose based on datasetcifar10_exps = piter @ {"data.name": "cifar10"} * cifar_models * trainingcifar100_exps = piter @ {"data.name": "cifar100"} * cifar_models * trainingimagenet_exps = piter @ {"data.name": "imagenet"} * imagenet_models * training# Combine allall_experiments = ( cifar10_exps.to_list() + cifar100_exps.to_list() + imagenet_exps.to_list())
API Reference
piter @ spec or piter(spec: dict) -> ParameterIterator
Create a parameter iterator from a specification dictionary.