Goal Programming Architecture¶
Deep dive into the goal programming module’s architecture and design patterns.
Design Philosophy¶
The goal programming module implements a declarative, data-driven approach to multi-objective optimization using four key design patterns:
Automatic Transformation: Hard constraints automatically convert to soft constraints
Semantic Indexing: Deviation variables indexed by Goal instances for business meaning
Flexible Solving: Support for both weighted (single-solve) and sequential (multi-solve) modes
Type Safety: Full generic type support throughout the module
Architecture Overview¶
Module Structure¶
graph TD
A[User Constraint] -->|.as_goal| B[LXGoalMetadata]
B --> C[Relaxation Module]
C --> D[RelaxedConstraint]
D --> E[Deviation Variables]
D --> F[Goal Instances]
D --> G[Equality Constraint]
E --> H[Objective Builder]
F --> H
H --> I[Weighted/Sequential]
I --> J[LXGoalProgrammingSolver]
J --> K[Solution with Deviations]
style A fill:#e1f5ff
style B fill:#fff4e1
style C fill:#ffe1e1
style D fill:#e1ffe1
style E fill:#f0e1ff
style F fill:#e8f4f8
style G fill:#ffe8e8
style H fill:#e8ffe8
style I fill:#f0e8ff
style J fill:#e8f4ff
style K fill:#fff0e8
Key modules:
goal.py: Data structures (LXGoal, LXGoalMetadata, LXGoalMode)relaxation.py: Constraint relaxation and deviation variable creationobjective_builder.py: Objective function constructionsolver.py: Sequential solving orchestration
Component Architecture¶
goal.py: Metadata and Data Structures¶
Purpose: Define goal programming metadata and configuration.
# Core data structures
class LXGoalMode(Enum):
WEIGHTED = "weighted" # Single solve
SEQUENTIAL = "sequential" # Multiple solves
@dataclass
class LXGoalMetadata:
priority: int # 0 = custom, 1+ = goal priorities
weight: float # Relative weight within priority
constraint_sense: LXConstraintSense # Original constraint type
undesired_deviations: Set[str] # {'pos', 'neg', or both}
@dataclass
class LXGoal:
id: str # Unique goal identifier
constraint_name: str # Original constraint name
priority: int # Priority level
weight: float # Goal weight
constraint_sense: LXConstraintSense
target_value: Optional[float] # RHS value if constant
instance_id: Optional[Any] # Original data instance ID
Design decisions:
LXGoal as data model: Deviation variables are indexed by LXGoal instances, providing semantic meaning to deviations (e.g., “Route 5 needs 3 buses” instead of “neg_dev[0] = 3”)
Automatic deviation determination:
__post_init__automatically setsundesired_deviationsbased onconstraint_sense(LE → pos, GE → neg, EQ → both)Priority 0 for custom objectives: Allows mixing traditional objectives with goal programming
relaxation.py: Constraint Transformation¶
Purpose: Transform hard constraints to soft constraints with deviation variables.
class RelaxedConstraint(Generic[TModel]):
constraint: LXConstraint[TModel] # Relaxed equality constraint
pos_deviation: LXVariable[LXGoal, float] # Positive deviation variable
neg_deviation: LXVariable[LXGoal, float] # Negative deviation variable
goal_metadata: LXGoalMetadata
goal_instances: List[LXGoal]
Transformation process:
Create Goal instances: One per constraint instance (or single goal for non-indexed)
Create deviation variables: Indexed by Goal instances, not original data
Build equality constraint:
expr + neg_dev - pos_dev = rhsPreserve metadata: Goal metadata and instances stored in RelaxedConstraint
Example transformation:
# Original: production >= demand
# Indexed by Product instances
# After relaxation:
# - Goal instances: [Goal("demand_A"), Goal("demand_B"), ...]
# - Variables: pos_dev[Goal("demand_A")], neg_dev[Goal("demand_A")], ...
# - Constraint: production[A] + neg_dev[Goal_A] - pos_dev[Goal_A] = demand[A]
Design decisions:
Generic type support:
RelaxedConstraint[TModel]maintains type safetyGoal instance creation: Maps constraint instances to Goal instances for semantic indexing
Variable naming convention:
{constraint_name}_{pos|neg}_dev
objective_builder.py: Objective Construction¶
Purpose: Build weighted or sequential objectives from relaxed constraints.
def build_weighted_objective(
relaxed_constraints: List[RelaxedConstraint],
base: float = 10.0,
exponent_offset: int = 6
) -> LXLinearExpression:
"""
Single objective with exponential priority scaling.
Priority 1 → 10^6
Priority 2 → 10^5
Priority 3 → 10^4
"""
def build_sequential_objectives(
relaxed_constraints: List[RelaxedConstraint]
) -> List[Tuple[int, LXLinearExpression]]:
"""
Multiple objectives for lexicographic optimization.
Returns: [(priority, objective), ...]
"""
Weight calculation:
def priority_to_weight(priority: int, base: float = 10.0,
exponent_offset: int = 6) -> float:
if priority == 0:
return 1.0 # Custom objectives
return base ** (exponent_offset - priority)
Design decisions:
Exponential scaling: Ensures higher priorities dominate lower priorities
Configurable base: Allow custom weight scaling if needed
Priority 0 handling: Custom objectives use weight 1.0 (no scaling)
Sequential excludes priority 0: Custom objectives handled separately
solver.py: Orchestration¶
Purpose: Orchestrate sequential (lexicographic) goal programming.
class LXGoalProgrammingSolver:
def __init__(self, optimizer: LXOptimizer):
self.optimizer = optimizer
def solve_sequential(
self, model: LXModel[TModel],
relaxed_constraints: List[RelaxedConstraint[TModel]],
**solver_params
) -> LXSolution[TModel]:
"""
Solve one priority at a time:
1. Optimize priority 1
2. Fix priority 1 deviations
3. Optimize priority 2
4. Repeat
"""
def solve_weighted(
self, model: LXModel[TModel],
**solver_params
) -> LXSolution[TModel]:
"""Pass-through to standard optimizer."""
Sequential solving algorithm:
Build objectives for each priority level
For each priority (sorted): a. Set objective for current priority b. Solve c. Record optimal deviation values d. Fix deviations as constraints (conceptually; currently via large weights)
Return final solution
Design decisions:
Weighted mode pass-through: Weighted mode is handled in LXModel, solver just calls optimizer
Sequential mode complexity: Sequential mode requires multiple solve iterations
Deviation fixing: Currently uses implicit fixing via weight dominance
Data Flow¶
Model Building Phase¶
sequenceDiagram
participant User
participant Constraint
participant Metadata
participant Relaxation
participant Model
User->>Constraint: .as_goal(priority, weight)
Constraint->>Metadata: Create LXGoalMetadata
Metadata-->>Constraint: Goal configuration
Constraint->>Model: Add to model
Note over Model: Stores goal metadata
Model->>Relaxation: relax_constraint()
Relaxation-->>Model: RelaxedConstraint
Key point: Relaxation happens when model is being prepared for solving, not during constraint definition.
Solving Phase (Weighted)¶
sequenceDiagram
participant Model
participant Objective
participant Solver
participant Solution
Model->>Model: Identify goal constraints
Model->>Objective: build_weighted_objective()
Objective-->>Model: Single objective expr
Model->>Solver: solve(model)
Solver->>Solver: Single optimization run
Solver-->>Solution: Optimal values + deviations
Solving Phase (Sequential)¶
sequenceDiagram
participant Solver
participant Objective
participant Optimizer
participant Model
Solver->>Objective: build_sequential_objectives()
Objective-->>Solver: [(p1, obj1), (p2, obj2), ...]
loop For each priority
Solver->>Model: Set objective = obj_p
Solver->>Optimizer: solve(model)
Optimizer-->>Solver: Solution at priority p
Solver->>Solver: Record deviation values
Note over Solver: Fix deviations for next priority
end
Solver-->>Solver: Return final solution
Type System¶
Generic Type Flow¶
TModel = TypeVar("TModel") # Original data model type
# Constraint with original type
constraint: LXConstraint[Product]
# Relaxed constraint maintains type
relaxed: RelaxedConstraint[Product]
# Deviation variables are indexed by LXGoal
pos_dev: LXVariable[LXGoal, float]
neg_dev: LXVariable[LXGoal, float]
# Goal instances map to original instances
goal: LXGoal
goal.instance_id: str # Product ID
# Solution maintains type
solution: LXSolution[Product]
Benefits:
Full IDE autocomplete for goal metadata
Type checking catches errors at development time
Self-documenting code through type annotations
Extension Points¶
Custom Goal Types¶
Extend LXGoalMetadata for specialized goal types:
@dataclass
class LXWeightedGoalMetadata(LXGoalMetadata):
"""Goal with dynamic weight calculation."""
weight_func: Callable[[Any], float]
def get_weight(self, instance: Any) -> float:
"""Calculate weight dynamically."""
return self.weight * self.weight_func(instance)
Custom Relaxation Strategies¶
Implement alternative relaxation approaches:
def relax_with_bounds(
constraint: LXConstraint[TModel],
metadata: LXGoalMetadata,
max_deviation: float
) -> RelaxedConstraint[TModel]:
"""Relax with bounded deviations."""
relaxed = relax_constraint(constraint, metadata)
# Add bounds to deviation variables
relaxed.pos_deviation.upper_bound = max_deviation
relaxed.neg_deviation.upper_bound = max_deviation
return relaxed
Custom Objective Builders¶
Create specialized objective construction:
def build_minimax_objective(
relaxed_constraints: List[RelaxedConstraint]
) -> Tuple[LXLinearExpression, LXVariable]:
"""
Minimax: Minimize maximum deviation.
Creates auxiliary variable z and constraints:
z >= deviation_i for all i
Objective: minimize z
"""
# Create max deviation variable
z = LXVariable[None, float]("max_dev").continuous().bounds(lower=0)
# Build constraints: z >= each deviation
max_constraints = []
for relaxed in relaxed_constraints:
# Implementation details...
# Objective: minimize z
objective = LXLinearExpression().add_term(z, coeff=1.0)
return objective, z
Performance Considerations¶
Memory Usage¶
Goal instances: One Goal instance per constraint instance
# For 1000 products with demand goals:
# - 1000 Goal instances (small objects)
# - 1000 pos_dev variables
# - 1000 neg_dev variables
# Memory: ~O(n) where n = constraint instances
Optimization:
Goal instances are lightweight dataclasses
Deviation variables created lazily during solving
No duplication of original data
Computational Complexity¶
Weighted mode: O(1) solves (single optimization)
Sequential mode: O(P) solves where P = number of priority levels
Trade-off:
Weighted: Faster but approximate priority enforcement
Sequential: Slower but strict lexicographic optimization
Testing Strategy¶
Unit Tests¶
Test individual components:
def test_goal_metadata_undesired_deviations():
"""Test automatic deviation determination."""
metadata_le = LXGoalMetadata(1, 1.0, LXConstraintSense.LE)
assert metadata_le.is_pos_undesired()
assert not metadata_le.is_neg_undesired()
metadata_ge = LXGoalMetadata(1, 1.0, LXConstraintSense.GE)
assert not metadata_ge.is_pos_undesired()
assert metadata_ge.is_neg_undesired()
def test_priority_to_weight():
"""Test weight scaling."""
assert priority_to_weight(0) == 1.0
assert priority_to_weight(1) == 1_000_000.0
assert priority_to_weight(2) == 100_000.0
Integration Tests¶
Test end-to-end workflows:
def test_weighted_goal_programming():
"""Test complete weighted GP workflow."""
model = build_test_model_with_goals()
solution = optimizer.solve(model)
assert solution.is_optimal()
# Verify goal achievement
assert solution.is_goal_satisfied("priority_1_goal")
# Priority 1 should have lower deviations than priority 2
dev_p1 = solution.get_total_deviation("priority_1_goal")
dev_p2 = solution.get_total_deviation("priority_2_goal")
assert dev_p1 <= dev_p2
Type Tests¶
Use mypy for static type checking:
mypy src/lumix/goal_programming
Common Patterns¶
Adding New Deviation Types¶
# Current: Binary undesired deviations ({'pos', 'neg'})
# Extension: Weighted deviations
@dataclass
class LXWeightedGoalMetadata(LXGoalMetadata):
pos_weight: float = 1.0
neg_weight: float = 1.0
def get_deviation_weights(self) -> Dict[str, float]:
weights = {}
if self.is_pos_undesired():
weights['pos'] = self.pos_weight
if self.is_neg_undesired():
weights['neg'] = self.neg_weight
return weights
Custom Solving Modes¶
class LXGoalMode(Enum):
WEIGHTED = "weighted"
SEQUENTIAL = "sequential"
HYBRID = "hybrid" # New: weighted within priorities, sequential across
Next Steps¶
Extending Goal Programming - How to extend the module
Design Decisions - Rationale for architectural choices
Goal Programming Module API - Full API reference
Goal Programming - User guide