Warning, /AID2E-framework/docs/CONSTRAINT_HANDLING.md is written in an unsupported language. File is not indexed.
0001 # Constraint Handling in AID2E
0002
0003 This document explains the complete constraint workflow in the AID2E framework, from definition to enforcement.
0004
0005 ## Overview
0006
0007 Constraints are handled through a three-layer architecture:
0008
0009 1. **DesignConfig** - Validates constraint syntax at configuration load time
0010 2. **SearchSpace** - Stores validated constraints for optimizer use
0011 3. **Optimizer** - Enforces constraints during candidate generation (optimizer-specific)
0012
0013 ## Architecture
0014
0015 ### 1. Constraint Definition (DesignConfig)
0016
0017 Constraints are defined in the design configuration as expressions over parameters:
0018
0019 ```yaml
0020 design_parameters:
0021 tracker:
0022 parameters:
0023 thickness:
0024 value: 1.0
0025 bounds: [0.5, 2.0]
0026 radius:
0027 value: 5.0
0028 bounds: [3.0, 10.0]
0029
0030 parameter_constraints:
0031 - name: "thickness_radius_limit"
0032 rule: "tracker.thickness + tracker.radius <= 10.0"
0033 - name: "minimum_radius"
0034 rule: "tracker.radius >= 4.0"
0035 ```
0036
0037 ### 2. Syntax Validation (DesignConfig)
0038
0039 When a `DesignConfig` is instantiated, constraints are automatically validated:
0040
0041 ```python
0042 from aid2e.utilities.configurations.design_config import DesignConfig
0043
0044 # This will validate all constraints
0045 config = DesignConfig(**config_data)
0046 ```
0047
0048 **Validation checks:**
0049 - ✅ Constraint rule is syntactically valid Python expression
0050 - ✅ All parameter names referenced in the rule exist in the design
0051 - ✅ Parameter names are properly qualified (e.g., `group.param`)
0052
0053 **Example errors caught:**
0054
0055 ```python
0056 # Unknown parameter
0057 rule: "tracker.unknown + tracker.radius <= 10.0"
0058 # ERROR: Unknown parameters: tracker.unknown
0059
0060 # Invalid syntax
0061 rule: "tracker.thickness +* tracker.radius <= 10.0"
0062 # ERROR: Invalid syntax in constraint
0063 ```
0064
0065 ### 3. Constraint Storage (SearchSpace)
0066
0067 Validated constraints are passed to the `SearchSpace`:
0068
0069 ```python
0070 from aid2e.optimizers.base import SearchSpace
0071
0072 # Create SearchSpace from validated DesignConfig
0073 search_space = SearchSpace.from_design_config(design_config)
0074
0075 # Constraints are now stored in search_space.constraints
0076 print(f"Constraints: {len(search_space.constraints)}")
0077 ```
0078
0079 ### 4. Runtime Validation (SearchSpace)
0080
0081 The `SearchSpace` provides runtime constraint checking for non-Ax optimizers:
0082
0083 ```python
0084 # Check if parameter values satisfy constraints
0085 param_values = {'tracker.thickness': 1.5, 'tracker.radius': 9.0}
0086 is_valid, errors = search_space.validate(param_values)
0087
0088 if not is_valid:
0089 print(f"Constraint violations: {errors}")
0090 ```
0091
0092 ### 5. Native Enforcement (Ax Optimizer)
0093
0094 The Ax optimizer converts constraints to Ax's native `ParameterConstraint` format:
0095
0096 ```python
0097 from aid2e.optimizers.ax.optimizer import AxOptimizer
0098 from aid2e.optimizers.ax.config import AxOptimizerConfig
0099
0100 # Create optimizer with constraints
0101 ax_config = AxOptimizerConfig(
0102 name="constrained_opt",
0103 n_initial_samples=10,
0104 model_type="SOBOL",
0105 objectives=["minimize:objective"]
0106 )
0107
0108 optimizer = AxOptimizer(
0109 search_space=search_space,
0110 config=ax_config,
0111 objective_names=["objective"]
0112 )
0113
0114 # Ax automatically enforces constraints during generation
0115 candidates = optimizer.suggest_candidates(n_candidates=5)
0116 # All candidates will satisfy constraints!
0117 ```
0118
0119 ## Constraint Format
0120
0121 ### Supported Operators
0122
0123 - `<=` - Less than or equal (upper bound)
0124 - `<` - Less than (strict upper bound)
0125 - `>=` - Greater than or equal (lower bound, converted to upper bound internally)
0126 - `>` - Greater than (strict lower bound, converted to upper bound internally)
0127
0128 ### Linear Constraints
0129
0130 Currently, only **linear constraints** are supported:
0131
0132 ```python
0133 # ✅ Valid: Simple sum with upper bound
0134 rule: "group.x + group.y <= 1.5"
0135
0136 # ✅ Valid: Weighted sum
0137 rule: "group.x + 2.0 * group.y <= 3.0"
0138
0139 # ✅ Valid: Lower bound (converted internally)
0140 rule: "group.x + group.y >= 0.5"
0141
0142 # ❌ Not supported: Non-linear constraints
0143 rule: "group.x * group.y <= 1.0"
0144 rule: "group.x ** 2 + group.y ** 2 <= 1.0"
0145 ```
0146
0147 ### Parameter Names
0148
0149 Parameter names must be fully qualified with group prefix:
0150
0151 ```python
0152 # ✅ Valid: Qualified names
0153 rule: "tracker.thickness + magnet.radius <= 10.0"
0154
0155 # ❌ Invalid: Unqualified names
0156 rule: "thickness + radius <= 10.0"
0157 ```
0158
0159 ## Implementation Details
0160
0161 ### ParameterConstraint Methods
0162
0163 The `ParameterConstraint` class provides three key methods:
0164
0165 #### 1. `extract_parameter_names() -> Set[str]`
0166
0167 Extracts all qualified parameter names from the constraint rule:
0168
0169 ```python
0170 constraint = ParameterConstraint(
0171 name="example",
0172 rule="tracker.x + magnet.y + detector.z <= 10.0"
0173 )
0174
0175 param_names = constraint.extract_parameter_names()
0176 # Returns: {'tracker.x', 'magnet.y', 'detector.z'}
0177 ```
0178
0179 #### 2. `validate_syntax(valid_param_names: Set[str]) -> Tuple[bool, Optional[str]]`
0180
0181 Validates constraint syntax and parameter existence:
0182
0183 ```python
0184 valid_params = {'tracker.x', 'magnet.y', 'detector.z'}
0185 is_valid, error_msg = constraint.validate_syntax(valid_params)
0186
0187 if not is_valid:
0188 print(f"Validation error: {error_msg}")
0189 ```
0190
0191 #### 3. `evaluate(param_values: Dict[str, Any]) -> bool`
0192
0193 Evaluates constraint at runtime:
0194
0195 ```python
0196 param_values = {'tracker.x': 3.0, 'magnet.y': 4.0, 'detector.z': 2.0}
0197 is_satisfied = constraint.evaluate(param_values)
0198 # Returns: True (3.0 + 4.0 + 2.0 = 9.0 <= 10.0)
0199 ```
0200
0201 ### Ax Constraint Conversion
0202
0203 The Ax optimizer converts constraint rules to Ax's `ParameterConstraint` format:
0204
0205 ```python
0206 # Design constraint
0207 rule: "group.x + group.y <= 1.5"
0208
0209 # Converted to Ax ParameterConstraint
0210 ax_constraint = ParameterConstraint(
0211 constraint_dict={'group.x': 1.0, 'group.y': 1.0},
0212 bound=1.5
0213 )
0214 ```
0215
0216 **Conversion logic:**
0217 1. Parse constraint rule to extract parameters and coefficients
0218 2. Create `constraint_dict` mapping parameter names to coefficients
0219 3. Extract bound value
0220 4. Handle `>=` and `>` by negating coefficients and bound
0221
0222 Example conversion:
0223
0224 ```python
0225 # Original: x + y >= 0.5
0226 # Converted: -x - y <= -0.5
0227 ax_constraint = ParameterConstraint(
0228 constraint_dict={'x': -1.0, 'y': -1.0},
0229 bound=-0.5
0230 )
0231 ```
0232
0233 ## Testing
0234
0235 Comprehensive tests verify constraint handling at all levels:
0236
0237 ### Syntax Validation Tests
0238
0239 ```python
0240 def test_valid_constraint_accepted():
0241 """Valid constraints are accepted."""
0242 # Test with: "group.x + group.y <= 1.5"
0243
0244 def test_unknown_parameter_rejected():
0245 """Constraints with unknown parameters are rejected."""
0246 # Test with: "group.x + group.unknown <= 1.0"
0247
0248 def test_syntax_error_rejected():
0249 """Constraints with invalid syntax are rejected."""
0250 # Test with: "group.x +* 1.0"
0251 ```
0252
0253 ### Runtime Validation Tests
0254
0255 ```python
0256 def test_evaluate_constraint():
0257 """Test runtime constraint evaluation."""
0258 constraint = ParameterConstraint(
0259 name="sum_limit",
0260 rule="group.x + group.y <= 1.5"
0261 )
0262
0263 # Satisfies constraint
0264 assert constraint.evaluate({'group.x': 0.5, 'group.y': 0.8}) is True
0265
0266 # Violates constraint
0267 assert constraint.evaluate({'group.x': 1.0, 'group.y': 0.6}) is False
0268 ```
0269
0270 ### Ax Enforcement Tests
0271
0272 ```python
0273 def test_ax_enforces_constraints():
0274 """Test that Ax enforces constraints during generation."""
0275 # Generate 20 candidates with constraint: x + y <= 1.5
0276 candidates = optimizer.suggest_candidates(n_candidates=20)
0277
0278 # Verify ALL candidates satisfy constraint
0279 for candidate in candidates:
0280 assert candidate['x'] + candidate['y'] <= 1.5
0281 ```
0282
0283 ## Best Practices
0284
0285 ### 1. Define Constraints Early
0286
0287 Validate constraints at configuration time to catch errors early:
0288
0289 ```python
0290 # ✅ Good: Errors caught immediately
0291 try:
0292 config = DesignConfig(**config_data)
0293 except ValidationError as e:
0294 print(f"Invalid constraints: {e}")
0295 ```
0296
0297 ### 2. Use Qualified Names
0298
0299 Always use fully qualified parameter names:
0300
0301 ```python
0302 # ✅ Good
0303 rule: "tracker.thickness + magnet.radius <= 10.0"
0304
0305 # ❌ Bad
0306 rule: "thickness + radius <= 10.0"
0307 ```
0308
0309 ### 3. Keep Constraints Linear
0310
0311 Stick to linear constraints for Ax compatibility:
0312
0313 ```python
0314 # ✅ Supported
0315 rule: "a + 2*b + 3*c <= 10.0"
0316
0317 # ❌ Not supported
0318 rule: "a * b <= 5.0"
0319 rule: "a ** 2 + b ** 2 <= 1.0"
0320 ```
0321
0322 ### 4. Test Constraint Enforcement
0323
0324 Always verify constraints are enforced:
0325
0326 ```python
0327 # Generate candidates
0328 candidates = optimizer.suggest_candidates(n_candidates=100)
0329
0330 # Verify constraints
0331 for candidate in candidates:
0332 is_valid, errors = search_space.validate(candidate)
0333 assert is_valid, f"Constraint violation: {errors}"
0334 ```
0335
0336 ## Limitations
0337
0338 ### Current Limitations
0339
0340 1. **Linear constraints only** - Non-linear constraints (products, powers) not supported
0341 2. **Simple operators** - Only `+`, `-`, `*` (with constants), `<=`, `>=`, `<`, `>`
0342 3. **Ax-specific** - Full native constraint support only for Ax optimizer
0343
0344 ### Future Enhancements
0345
0346 Potential improvements:
0347 - Support for non-linear constraints (via penalty methods or constraint-aware sampling)
0348 - More complex expressions (absolute values, min/max, etc.)
0349 - Constraint propagation and simplification
0350 - Automatic constraint tightening based on feasibility
0351
0352 ## Example: Complete Workflow
0353
0354 Here's a complete example showing the entire constraint workflow:
0355
0356 ```python
0357 from aid2e.utilities.configurations.design_config import DesignConfig
0358 from aid2e.optimizers.base import SearchSpace
0359 from aid2e.optimizers.ax.optimizer import AxOptimizer
0360 from aid2e.optimizers.ax.config import AxOptimizerConfig
0361
0362 # 1. Define design with constraints
0363 config_data = {
0364 "design_parameters": {
0365 "tracker": {
0366 "parameters": {
0367 "thickness": {"value": 1.0, "bounds": [0.5, 2.0]},
0368 "radius": {"value": 5.0, "bounds": [3.0, 10.0]},
0369 }
0370 }
0371 },
0372 "parameter_constraints": [
0373 {"name": "total_limit", "rule": "tracker.thickness + tracker.radius <= 10.0"},
0374 {"name": "min_radius", "rule": "tracker.radius >= 4.0"}
0375 ]
0376 }
0377
0378 # 2. Create and validate DesignConfig
0379 design_config = DesignConfig(**config_data)
0380 print(f"✅ Config validated with {len(design_config.parameter_constraints)} constraints")
0381
0382 # 3. Create SearchSpace
0383 search_space = SearchSpace.from_design_config(design_config)
0384 print(f"✅ SearchSpace created with {len(search_space.constraints)} constraints")
0385
0386 # 4. Create optimizer
0387 ax_config = AxOptimizerConfig(
0388 name="constrained_optimization",
0389 n_initial_samples=10,
0390 model_type="SOBOL",
0391 objectives=["objective"]
0392 )
0393
0394 optimizer = AxOptimizer(
0395 search_space=search_space,
0396 config=ax_config,
0397 objective_names=["objective"]
0398 )
0399 print("✅ Optimizer created with native constraint enforcement")
0400
0401 # 5. Generate candidates (constraints automatically enforced!)
0402 candidates = optimizer.suggest_candidates(n_candidates=20)
0403 print(f"✅ Generated {len(candidates)} candidates")
0404
0405 # 6. Verify constraints (optional - Ax already enforces them)
0406 violations = 0
0407 for i, candidate in enumerate(candidates):
0408 thickness = candidate['tracker.thickness']
0409 radius = candidate['tracker.radius']
0410
0411 # Check constraint 1: thickness + radius <= 10.0
0412 if thickness + radius > 10.0:
0413 violations += 1
0414 print(f"❌ Candidate {i}: total={thickness + radius:.2f} > 10.0")
0415
0416 # Check constraint 2: radius >= 4.0
0417 if radius < 4.0:
0418 violations += 1
0419 print(f"❌ Candidate {i}: radius={radius:.2f} < 4.0")
0420
0421 if violations == 0:
0422 print("✅ All candidates satisfy constraints!")
0423 else:
0424 print(f"⚠️ Found {violations} constraint violations")
0425 ```
0426
0427 ## See Also
0428
0429 - [DesignConfig API Documentation](api-reference/utilities.md)
0430 - [SearchSpace API Documentation](api-reference/optimizers.md)
0431 - [AxOptimizer API Documentation](api-reference/optimizers.md)
0432 - [Test Suite](https://github.com/aid2e/AID2E-framework/blob/main/tests/test_optimizers/test_constraint_integration.py)