Spaces:
Sleeping
Sleeping
| """ | |
| Constraint definitions for your optimization problem. | |
| Constraints are defined using a fluent API: | |
| 1. for_each(Entity) - iterate over all entities | |
| 2. filter(predicate) - keep only matching entities | |
| 3. join(OtherEntity, ...) - combine with other entities | |
| 4. group_by(key, collector) - aggregate by key | |
| 5. penalize(weight) or reward(weight) - affect the score | |
| TODO: Replace these example constraints with your own business rules. | |
| """ | |
| from solverforge_legacy.solver.score import ( | |
| constraint_provider, | |
| ConstraintFactory, | |
| Joiners, | |
| HardSoftScore, | |
| ConstraintCollectors, | |
| ) | |
| from .domain import Resource, Task | |
| # ============================================================================= | |
| # CONSTRAINT WEIGHTS | |
| # ============================================================================= | |
| # Global weights that can be adjusted at runtime via the REST API. | |
| # Weight 0 = disabled, 100 = full strength. | |
| # Set by rest_api.py before solving starts. | |
| CONSTRAINT_WEIGHTS = { | |
| 'required_skill': 100, # Hard constraint | |
| 'resource_capacity': 100, # Hard constraint | |
| 'minimize_duration': 50, # Soft constraint | |
| 'balance_load': 50, # Soft constraint | |
| } | |
| def get_weight(name: str) -> int: | |
| """Get the weight for a constraint (0-100 scale).""" | |
| return CONSTRAINT_WEIGHTS.get(name, 100) | |
| def define_constraints(constraint_factory: ConstraintFactory): | |
| """ | |
| Define all constraints for the optimization problem. | |
| Returns a list of constraints, evaluated in order: | |
| - Hard constraints: Must be satisfied (score < 0 = infeasible) | |
| - Soft constraints: Should be optimized (higher = better) | |
| """ | |
| return [ | |
| # Hard constraints (must be satisfied) | |
| required_skill(constraint_factory), | |
| resource_capacity(constraint_factory), | |
| # Soft constraints (optimize these) | |
| minimize_total_duration(constraint_factory), | |
| balance_resource_load(constraint_factory), | |
| ] | |
| # ============================================================================= | |
| # HARD CONSTRAINTS | |
| # ============================================================================= | |
| def required_skill(constraint_factory: ConstraintFactory): | |
| """ | |
| Hard: Each task must be assigned to a resource with the required skill. | |
| Pattern: for_each -> filter -> penalize | |
| NOTE: We check task.resource is not None FIRST, because unassigned tasks | |
| should not be penalized - they're just not yet assigned. | |
| NOTE: We use len(str(task.required_skill)) > 0 instead of just task.required_skill | |
| because the value may be a Java String object which doesn't work with Python's | |
| boolean operators directly. | |
| WEIGHT: When weight=0, this constraint is effectively disabled. | |
| """ | |
| weight = get_weight('required_skill') | |
| if weight == 0: | |
| # Return a no-op constraint when disabled | |
| return ( | |
| constraint_factory.for_each(Task) | |
| .filter(lambda task: False) # Never matches | |
| .penalize(HardSoftScore.ONE_HARD) | |
| .as_constraint("Required skill missing") | |
| ) | |
| return ( | |
| constraint_factory.for_each(Task) | |
| .filter(lambda task: task.resource is not None | |
| and len(str(task.required_skill)) > 0 | |
| and not task.has_required_skill()) | |
| .penalize(HardSoftScore.ONE_HARD, lambda task: weight) | |
| .as_constraint("Required skill missing") | |
| ) | |
| def resource_capacity(constraint_factory: ConstraintFactory): | |
| """ | |
| Hard: Total task duration per resource must not exceed capacity. | |
| Pattern: for_each -> group_by -> filter -> penalize | |
| WEIGHT: When weight=0, this constraint is effectively disabled. | |
| """ | |
| weight = get_weight('resource_capacity') | |
| if weight == 0: | |
| return ( | |
| constraint_factory.for_each(Task) | |
| .filter(lambda task: False) | |
| .penalize(HardSoftScore.ONE_HARD) | |
| .as_constraint("Resource capacity exceeded") | |
| ) | |
| return ( | |
| constraint_factory.for_each(Task) | |
| .group_by( | |
| lambda task: task.resource, | |
| ConstraintCollectors.sum(lambda task: task.duration) | |
| ) | |
| .filter(lambda resource, total_duration: | |
| resource is not None and total_duration > resource.capacity) | |
| .penalize( | |
| HardSoftScore.ONE_HARD, | |
| lambda resource, total_duration: (total_duration - resource.capacity) * weight // 100 | |
| ) | |
| .as_constraint("Resource capacity exceeded") | |
| ) | |
| # ============================================================================= | |
| # SOFT CONSTRAINTS | |
| # ============================================================================= | |
| def minimize_total_duration(constraint_factory: ConstraintFactory): | |
| """ | |
| Soft: Prefer shorter total duration (makespan). | |
| Pattern: for_each -> penalize with weight function | |
| WEIGHT: Penalty multiplied by weight/100. | |
| """ | |
| weight = get_weight('minimize_duration') | |
| if weight == 0: | |
| return ( | |
| constraint_factory.for_each(Task) | |
| .filter(lambda task: False) | |
| .penalize(HardSoftScore.ONE_SOFT) | |
| .as_constraint("Minimize total duration") | |
| ) | |
| return ( | |
| constraint_factory.for_each(Task) | |
| .filter(lambda task: task.resource is not None) | |
| .penalize(HardSoftScore.ONE_SOFT, lambda task: task.duration * weight // 100) | |
| .as_constraint("Minimize total duration") | |
| ) | |
| def balance_resource_load(constraint_factory: ConstraintFactory): | |
| """ | |
| Soft: Balance workload fairly across all resources. | |
| Pattern: for_each -> group_by -> complement -> group_by(loadBalance) -> penalize | |
| WEIGHT: Penalty multiplied by weight/100. | |
| """ | |
| weight = get_weight('balance_load') | |
| if weight == 0: | |
| return ( | |
| constraint_factory.for_each(Task) | |
| .filter(lambda task: False) | |
| .penalize(HardSoftScore.ONE_SOFT) | |
| .as_constraint("Balance resource load") | |
| ) | |
| return ( | |
| constraint_factory.for_each(Task) | |
| .group_by( | |
| lambda task: task.resource, | |
| ConstraintCollectors.sum(lambda task: task.duration) | |
| ) | |
| .complement(Resource, lambda r: 0) # Include resources with 0 tasks | |
| .group_by( | |
| ConstraintCollectors.load_balance( | |
| lambda resource, duration: resource, | |
| lambda resource, duration: duration, | |
| ) | |
| ) | |
| .penalize( | |
| HardSoftScore.ONE_SOFT, | |
| lambda load_balance: int(load_balance.unfairness()) * weight // 100 | |
| ) | |
| .as_constraint("Balance resource load") | |
| ) | |
| # ============================================================================= | |
| # ADDITIONAL CONSTRAINT PATTERNS (commented examples) | |
| # ============================================================================= | |
| # def no_overlapping_tasks(constraint_factory: ConstraintFactory): | |
| # """ | |
| # Example: Two tasks on same resource cannot overlap in time. | |
| # | |
| # Pattern: for_each_unique_pair with Joiners | |
| # """ | |
| # return ( | |
| # constraint_factory.for_each_unique_pair( | |
| # Task, | |
| # Joiners.equal(lambda task: task.resource), | |
| # Joiners.overlapping( | |
| # lambda task: task.start_time, | |
| # lambda task: task.end_time | |
| # ), | |
| # ) | |
| # .penalize(HardSoftScore.ONE_HARD) | |
| # .as_constraint("Overlapping tasks") | |
| # ) | |
| # def preferred_resource(constraint_factory: ConstraintFactory): | |
| # """ | |
| # Example: Reward tasks assigned to their preferred resource. | |
| # | |
| # Pattern: for_each -> filter -> reward | |
| # """ | |
| # return ( | |
| # constraint_factory.for_each(Task) | |
| # .filter(lambda task: task.resource == task.preferred_resource) | |
| # .reward(HardSoftScore.ONE_SOFT) | |
| # .as_constraint("Preferred resource") | |
| # ) | |