Back to home page

EIC code displayed by LXR

 
 

    


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)