Goal Programming Solutions

Learn how to work with goal programming solutions and analyze goal deviations.

Overview

Goal programming extends traditional optimization by allowing you to define multiple objectives as goals with target values. Instead of strict constraints, goals can be violated with penalties.

The LXSolution class provides methods to:

  • Access deviation values (positive and negative)

  • Check if goals are satisfied

  • Calculate total deviations

  • Analyze goal achievement

Goal Programming Concepts

What Are Goals?

A goal is a soft constraint that you’d like to achieve but can deviate from:

# Traditional hard constraint
production >= demand  # Must be satisfied

# Goal constraint
production ≈ demand_target  # Try to achieve, but can deviate
# Creates two deviation variables:
#   - positive deviation (over-achievement)
#   - negative deviation (under-achievement)

Deviation Variables

Each goal creates two deviation variables:

  • Positive deviation (pos): Amount by which goal is exceeded

  • Negative deviation (neg``): Amount by which goal is under-achieved

# Example: demand_target = 1000
# If production = 1100:
#   pos = 100 (over-production)
#   neg = 0 (no under-production)

# If production = 900:
#   pos = 0 (no over-production)
#   neg = 100 (under-production)

Accessing Goal Deviations

Get Deviations

# Get deviations for a goal
deviations = solution.get_goal_deviations("production_target")

if deviations:
    pos_dev = deviations["pos"]  # Over-achievement
    neg_dev = deviations["neg"]  # Under-achievement

    print(f"Positive deviation: {pos_dev}")
    print(f"Negative deviation: {neg_dev}")
else:
    print("Goal 'production_target' not found")

Return Value

The get_goal_deviations() method returns a dictionary:

{
    "pos": Union[float, Dict[Any, float]],
    "neg": Union[float, Dict[Any, float]]
}

For scalar goals:

deviations = solution.get_goal_deviations("total_cost_target")
# {'pos': 0.0, 'neg': 150.5}

For indexed goals:

deviations = solution.get_goal_deviations("demand_target")
# {
#     'pos': {'product_A': 10.0, 'product_B': 0.0},
#     'neg': {'product_A': 0.0, 'product_B': 5.0}
# }

Checking Goal Satisfaction

is_goal_satisfied()

Check if a goal is achieved within tolerance:

# Check with default tolerance (1e-6)
if solution.is_goal_satisfied("production_target"):
    print("Production target achieved!")

# Check with custom tolerance
if solution.is_goal_satisfied("quality_target", tolerance=0.01):
    print("Quality target achieved (within 1% tolerance)")

Return Values

  • True: Goal is satisfied (both deviations within tolerance)

  • False: Goal is not satisfied

  • None: Goal not found in solution

satisfied = solution.is_goal_satisfied("demand_target")

if satisfied is True:
    print("Goal achieved")
elif satisfied is False:
    print("Goal not achieved")
else:
    print("Goal not found in solution")

Total Deviation

Get the sum of absolute deviations:

total_dev = solution.get_total_deviation("production_target")

if total_dev is not None:
    print(f"Total deviation: {total_dev:.2f}")
else:
    print("Goal not found")

Calculation:

# For scalar goals
total_deviation = abs(pos_dev) + abs(neg_dev)

# For indexed goals
total_deviation = sum(abs(v) for v in pos_dev.values()) + \\
                  sum(abs(v) for v in neg_dev.values())

Working with Goal Solutions

Analyzing Goal Achievement

def analyze_goal_achievement(solution, goal_names):
    """Analyze which goals are achieved."""

    print("Goal Achievement Analysis")
    print(f"{'Goal':<30} {'Satisfied':<12} {'Total Deviation'}")
    print("-" * 70)

    satisfied_count = 0

    for goal_name in goal_names:
        satisfied = solution.is_goal_satisfied(goal_name)
        total_dev = solution.get_total_deviation(goal_name)

        if satisfied:
            satisfied_count += 1
            status = "✓ Yes"
        else:
            status = "✗ No"

        dev_str = f"{total_dev:.2f}" if total_dev else "N/A"
        print(f"{goal_name:<30} {status:<12} {dev_str}")

    print(f"\nGoals achieved: {satisfied_count}/{len(goal_names)}")

Prioritizing Unmet Goals

def prioritize_unmet_goals(solution, goals):
    """Rank goals by deviation for follow-up action."""

    unmet_goals = []

    for goal_name in goals:
        if not solution.is_goal_satisfied(goal_name):
            total_dev = solution.get_total_deviation(goal_name)
            if total_dev:
                unmet_goals.append((goal_name, total_dev))

    # Sort by deviation (largest first)
    unmet_goals.sort(key=lambda x: x[1], reverse=True)

    print("Unmet Goals (prioritized by deviation):")
    for goal_name, deviation in unmet_goals:
        print(f"  {goal_name}: {deviation:.2f}")

        # Show breakdown
        deviations = solution.get_goal_deviations(goal_name)
        if deviations:
            print(f"    Positive: {deviations['pos']}")
            print(f"    Negative: {deviations['neg']}")

Deviation Breakdown

For indexed goals, analyze deviations by index:

def analyze_indexed_goal(solution, goal_name):
    """Detailed analysis of indexed goal deviations."""

    deviations = solution.get_goal_deviations(goal_name)

    if not deviations:
        print(f"Goal '{goal_name}' not found")
        return

    pos_dev = deviations["pos"]
    neg_dev = deviations["neg"]

    # Check if indexed
    if isinstance(pos_dev, dict):
        print(f"Goal: {goal_name} (indexed)")
        print(f"\n{'Index':<20} {'Pos Deviation':<18} {'Neg Deviation':<18} {'Status'}")
        print("-" * 80)

        all_keys = set(pos_dev.keys()) | set(neg_dev.keys())

        for key in sorted(all_keys):
            pos = pos_dev.get(key, 0)
            neg = neg_dev.get(key, 0)

            if abs(pos) < 1e-6 and abs(neg) < 1e-6:
                status = "Satisfied"
            elif pos > 1e-6:
                status = f"Over by {pos:.2f}"
            else:
                status = f"Under by {neg:.2f}"

            print(f"{str(key):<20} {pos:<18.2f} {neg:<18.2f} {status}")
    else:
        # Scalar goal
        print(f"Goal: {goal_name} (scalar)")
        print(f"Positive deviation: {pos_dev:.2f}")
        print(f"Negative deviation: {neg_dev:.2f}")

Solution Summary with Goals

The summary() method includes goal information:

print(solution.summary())

Output:

Status: optimal
Objective: 12345.678900
Solve time: 0.123s
Non-zero variables: 42/100

Goal Constraints: 5
Goals Satisfied: 3/5

Example Workflows

Production Planning with Goals

from lumix import LXModel, LXVariable, LXConstraint

# Define variables
production = LXVariable[Product, float]("production").from_data(products)

# Define goals
demand_goal = (
    LXConstraint[Product]("demand_goal")
    .expression(...)
    .eq()
    .rhs(lambda p: p.demand_target)
    .as_goal(priority=1)  # Higher priority
    .from_data(products)
)

quality_goal = (
    LXConstraint("quality_goal")
    .expression(...)
    .ge()
    .rhs(0.95)
    .as_goal(priority=2)  # Lower priority
)

# Build and solve
model = LXModel("production_with_goals")
model.add_variable(production)
model.add_constraint(demand_goal)
model.add_constraint(quality_goal)

solution = optimizer.solve(model)

# Analyze results
if solution.is_optimal():
    print("=== Goal Achievement ===")

    # Check demand goals
    if solution.is_goal_satisfied("demand_goal"):
        print("✓ All demand targets met")
    else:
        print("✗ Some demand targets missed")
        analyze_indexed_goal(solution, "demand_goal")

    # Check quality goal
    if solution.is_goal_satisfied("quality_goal"):
        print("✓ Quality target met")
    else:
        deviations = solution.get_goal_deviations("quality_goal")
        if deviations["neg"] > 0:
            print(f"✗ Quality {deviations['neg']:.2%} below target")

Multi-Objective Optimization

def solve_multi_objective(products, priorities):
    """Solve with multiple conflicting objectives."""

    # Goals with different priorities
    goals = {
        "maximize_profit": {
            "priority": 1,
            "weight": 1.0,
            "type": "maximize"
        },
        "minimize_waste": {
            "priority": 2,
            "weight": 0.5,
            "type": "minimize"
        },
        "meet_demand": {
            "priority": 1,
            "weight": 1.0,
            "type": "target"
        }
    }

    # Build model with goals...
    solution = optimizer.solve(model)

    # Analyze trade-offs
    print("=== Multi-Objective Results ===")
    print(f"Objective value: {solution.objective_value:.2f}")
    print()

    for goal_name, config in goals.items():
        satisfied = solution.is_goal_satisfied(goal_name)
        total_dev = solution.get_total_deviation(goal_name)

        status = "✓" if satisfied else "✗"
        print(f"{status} {goal_name} (priority {config['priority']})")

        if not satisfied and total_dev:
            print(f"   Deviation: {total_dev:.2f}")
            print(f"   Weight: {config['weight']}")
            print(f"   Weighted penalty: {total_dev * config['weight']:.2f}")

Sequential Goal Programming

def sequential_goal_programming(products, goal_priorities):
    """Solve goals sequentially by priority."""

    # Priority levels (1 = highest)
    priorities = sorted(set(goal_priorities.values()))

    solutions_by_priority = {}

    for priority in priorities:
        print(f"\\n=== Solving Priority {priority} Goals ===")

        # Get goals at this priority
        current_goals = [
            name for name, p in goal_priorities.items()
            if p == priority
        ]

        # Build and solve model for this priority
        model = build_model_with_goals(current_goals)
        solution = optimizer.solve(model)

        solutions_by_priority[priority] = solution

        # Report results
        for goal_name in current_goals:
            satisfied = solution.is_goal_satisfied(goal_name)
            status = "✓" if satisfied else "✗"
            print(f"{status} {goal_name}")

            if not satisfied:
                total_dev = solution.get_total_deviation(goal_name)
                print(f"   Deviation: {total_dev:.2f}")

    return solutions_by_priority

Handling Deviation Types

Asymmetric Goals

Sometimes you only care about one type of deviation:

def check_one_sided_goal(solution, goal_name, direction="under"):
    """Check goals where only one direction matters."""

    deviations = solution.get_goal_deviations(goal_name)

    if not deviations:
        return None

    if direction == "under":
        # Only care about under-achievement (negative deviation)
        total_under = deviations["neg"]
        if isinstance(total_under, dict):
            total_under = sum(total_under.values())

        if total_under < 1e-6:
            print(f"✓ {goal_name}: No under-achievement")
        else:
            print(f"✗ {goal_name}: Under by {total_under:.2f}")

    elif direction == "over":
        # Only care about over-achievement (positive deviation)
        total_over = deviations["pos"]
        if isinstance(total_over, dict):
            total_over = sum(total_over.values())

        if total_over < 1e-6:
            print(f"✓ {goal_name}: No over-achievement")
        else:
            print(f"✗ {goal_name}: Over by {total_over:.2f}")

Weighted Deviations

Calculate weighted deviation for cost analysis:

def calculate_weighted_deviation(solution, goal_name, pos_weight, neg_weight):
    """Calculate deviation with asymmetric weights."""

    deviations = solution.get_goal_deviations(goal_name)

    if not deviations:
        return None

    pos_dev = deviations["pos"]
    neg_dev = deviations["neg"]

    # Handle scalar vs indexed
    if isinstance(pos_dev, dict):
        total_pos = sum(pos_dev.values())
        total_neg = sum(neg_dev.values())
    else:
        total_pos = pos_dev
        total_neg = neg_dev

    weighted_total = total_pos * pos_weight + total_neg * neg_weight

    print(f"{goal_name}:")
    print(f"  Positive deviation: {total_pos:.2f} × {pos_weight} = {total_pos * pos_weight:.2f}")
    print(f"  Negative deviation: {total_neg:.2f} × {neg_weight} = {total_neg * neg_weight:.2f}")
    print(f"  Total weighted deviation: {weighted_total:.2f}")

    return weighted_total

Best Practices

  1. Always Check Goal Existence

    deviations = solution.get_goal_deviations("my_goal")
    if deviations is None:
        print("Goal not found")
        return
    
  2. Use Appropriate Tolerance

    # For percentage goals
    satisfied = solution.is_goal_satisfied("quality", tolerance=0.01)  # 1%
    
    # For absolute goals
    satisfied = solution.is_goal_satisfied("demand", tolerance=1.0)  # 1 unit
    
  3. Handle Both Scalar and Indexed Goals

    deviations = solution.get_goal_deviations("my_goal")
    pos_dev = deviations["pos"]
    
    # Check if indexed
    if isinstance(pos_dev, dict):
        total = sum(pos_dev.values())
    else:
        total = pos_dev
    
  4. Report Goal Achievement Clearly

    def report_goals(solution, goal_names):
        achieved = sum(1 for g in goal_names if solution.is_goal_satisfied(g))
        total = len(goal_names)
        pct = (achieved / total) * 100
    
        print(f"Goals Achieved: {achieved}/{total} ({pct:.1f}%)")
    

Common Patterns

Dashboard Summary

def goal_programming_dashboard(solution, goals_config):
    """Create dashboard summary of goal achievement."""

    print("=" * 80)
    print("GOAL PROGRAMMING DASHBOARD".center(80))
    print("=" * 80)

    by_priority = {}
    for goal_name, config in goals_config.items():
        priority = config.get("priority", 1)
        if priority not in by_priority:
            by_priority[priority] = []
        by_priority[priority].append(goal_name)

    for priority in sorted(by_priority.keys()):
        print(f"\\nPriority {priority} Goals:")
        print("-" * 80)

        for goal_name in by_priority[priority]:
            satisfied = solution.is_goal_satisfied(goal_name)
            total_dev = solution.get_total_deviation(goal_name)

            status_icon = "✓" if satisfied else "✗"
            status_text = "Satisfied" if satisfied else f"Deviation: {total_dev:.2f}"

            print(f"{status_icon} {goal_name:<40} {status_text}")

    print("=" * 80)

Next Steps