Skip to content

Analysis Examples

This guide provides comprehensive examples of using OptiScope's analysis tools to extract insights from optimization results.

Pareto Front Identification

Identify non-dominated solutions from your optimization results.

Basic Usage

from optiscope.analysis import identify_pareto_front
from optiscope.core.result_set import ResultSet
import numpy as np

# Identify Pareto-optimal solutions
pareto_mask = identify_pareto_front(
    objectives=result.objectives,
    directions=result.metadata.optimization_directions
)

print(f"Found {pareto_mask.sum()} Pareto-optimal solutions out of {len(result.objectives)}")

# Create a result set from the Pareto front
pareto_set = ResultSet(
    name="pareto_front",
    indices=np.where(pareto_mask)[0].tolist(),
    created_by="pareto_analysis",
    description="Non-dominated solutions"
)

# Add to result
result.add_set(pareto_set)

# Access Pareto solutions
pareto_objectives = result.objectives[pareto_mask]
print(f"Pareto front objectives:\n{pareto_objectives}")

With Feasibility Filtering

Only consider feasible solutions when identifying the Pareto front:

import numpy as np

# Create feasibility mask (all constraints <= 0)
if result.constraints is not None:
    feasible_mask = np.all(result.constraints <= 0, axis=1)
else:
    feasible_mask = np.ones(len(result.objectives), dtype=bool)

# Identify Pareto front among feasible solutions
pareto_mask = identify_pareto_front(
    objectives=result.objectives,
    directions=result.metadata.optimization_directions,
    mask=feasible_mask
)

print(f"Found {pareto_mask.sum()} feasible Pareto-optimal solutions")

Smart Pareto Filter

Reduce large Pareto fronts to a representative subset while preserving diversity.

Manual Epsilon Specification

from optiscope.analysis import smart_pareto_filter
import numpy as np

# First, get the Pareto front
pareto_mask = identify_pareto_front(
    objectives=result.objectives,
    directions=result.metadata.optimization_directions
)
pareto_indices = np.where(pareto_mask)[0]
pareto_objectives = result.objectives.iloc[pareto_indices]

print(f"Pareto front has {len(pareto_objectives)} solutions")

# Apply Smart Pareto Filter with manual epsilon
# Epsilon controls the minimum separation between selected points
filtered_indices = smart_pareto_filter(
    objectives=pareto_objectives,
    epsilon=0.05,  # 5% minimum separation in normalized space
    normalize=True
)

print(f"Smart Pareto Filter reduced to {len(filtered_indices)} solutions")

# Map back to original indices
final_indices = pareto_indices[filtered_indices]

# Create result set
result.add_set(ResultSet(
    name="filtered_pareto_manual",
    indices=final_indices.tolist(),
    created_by="smart_pareto_filter",
    description=f"Filtered Pareto front with epsilon=0.05 ({len(filtered_indices)} points)"
))

Adaptive Smart Pareto Filter

Automatically determine the best epsilon based on target reduction:

from optiscope.analysis import adaptive_smart_pareto_filter
import numpy as np

# Get Pareto front
pareto_mask = identify_pareto_front(
    objectives=result.objectives,
    directions=result.metadata.optimization_directions
)
pareto_indices = np.where(pareto_mask)[0]
pareto_objectives = result.objectives.iloc[pareto_indices]

# Apply adaptive filter
pareto_indices = np.where(pareto_mask)[0]
pareto_objectives = result.objectives.iloc[pareto_indices]

# Apply adaptive filter
filtered_indices = adaptive_smart_pareto_filter(
    objectives=pareto_objectives,
    target_reduction=0.5,  # Reduce to 50% of original
    min_points=10,  # Keep at least 10 points
    max_points=50,  # Keep at most 50 points
    normalize=True
)

print(f"Adaptive filter reduced from {len(pareto_objectives)} to {len(filtered_indices)} solutions")

# Create result set
final_indices = pareto_indices[filtered_indices]
result.add_set(ResultSet(
    name="filtered_pareto",
    indices=final_indices.tolist(),
    created_by="adaptive_smart_pareto_filter",
    description=f"Representative subset of Pareto front ({len(filtered_indices)} points)"
))

Visualizing Filter Effect

from optiscope.analysis import visualize_filter_effect

# Visualize the effect of filtering (for 2D or 3D objectives)
fig = visualize_filter_effect(
    objectives=pareto_objectives,
    filtered_indices=filtered_indices,
    directions=result.metadata.optimization_directions
)
fig.show()

TOPSIS (Multi-Criteria Decision Analysis)

Rank solutions based on your preferences using TOPSIS.

Basic TOPSIS

from optiscope.analysis import topsis
import numpy as np

# Define weights for each objective
# Weights should sum to 1.0 and represent relative importance
weights = [0.4, 0.3, 0.3]  # 40% weight on first objective, 30% on others

# Calculate TOPSIS scores
scores = topsis(
    objectives=result.objectives,
    weights=weights,
    directions=result.metadata.optimization_directions,
    normalize_method="vector"  # or "minmax"
)

# Get the best solution
best_idx = np.argmax(scores)
print(f"Best solution index: {best_idx}")
print(f"TOPSIS score: {scores[best_idx]:.3f}")
print(f"Objectives: {result.objectives.iloc[best_idx]}")

# Get top 10 solutions
top_10_indices = np.argsort(scores)[-10:][::-1]
print(f"\nTop 10 solutions:")
for i, idx in enumerate(top_10_indices):
    print(f"{i+1}. Index {idx}: Score = {scores[idx]:.3f}")

Detailed TOPSIS Results

# Get detailed results including distances and ranking
details = topsis(
    objectives=result.objectives,
    weights=[0.4, 0.3, 0.3],
    directions=result.metadata.optimization_directions,
    return_details=True
)

print(f"Ideal solution: {details['ideal']}")
print(f"Anti-ideal solution: {details['anti_ideal']}")
print(f"\nTop 5 solutions:")
for i, idx in enumerate(details['ranking'][:5]):
    print(f"{i+1}. Index {idx}:")
    print(f"   Score: {details['scores'][idx]:.3f}")
    print(f"   Distance to ideal: {details['distance_to_ideal'][idx]:.3f}")
    print(f"   Distance to anti-ideal: {details['distance_to_anti_ideal'][idx]:.3f}")

# Create result set for top solutions
result.add_set(ResultSet(
    name="top_10_topsis",
    indices=details['ranking'][:10].tolist(),
    created_by="topsis",
    description=f"Top 10 solutions by TOPSIS (weights: {weights})"
))

Comparing Weight Scenarios

Compare how different stakeholder preferences affect rankings:

from optiscope.analysis import compare_weight_scenarios
import numpy as np

# Define different weight scenarios
scenarios = {
    "Cost-focused": np.array([0.6, 0.2, 0.2]),
    "Performance-focused": np.array([0.2, 0.6, 0.2]),
    "Balanced": np.array([0.33, 0.33, 0.34]),
    "Quality-focused": np.array([0.2, 0.2, 0.6])
}

# Compare scenarios
comparison = compare_weight_scenarios(
    objectives=result.objectives,
    weight_scenarios=scenarios,
    directions=result.metadata.optimization_directions,
    top_n=5  # Show top 5 for each scenario
)

print("Top solutions for each scenario:")
print(comparison)

# Find consensus solutions (appear in top 5 for all scenarios)
top_indices_per_scenario = []
for scenario_name in scenarios.keys():
    scenario_top = comparison[comparison['Scenario'] == scenario_name]['Solution_Index'].values
    top_indices_per_scenario.append(set(scenario_top))

consensus = set.intersection(*top_indices_per_scenario)
print(f"\nConsensus solutions (in top 5 for all scenarios): {consensus}")

Sensitivity Analysis

Analyze how sensitive rankings are to weight changes:

from optiscope.analysis import sensitivity_analysis_topsis
import numpy as np

# Define base weights
base_weights = np.array([0.4, 0.3, 0.3])

# Perform sensitivity analysis
sensitivity = sensitivity_analysis_topsis(
    objectives=result.objectives,
    base_weights=base_weights,
    directions=result.metadata.optimization_directions,
    perturbation=0.1,  # ±10% variation
    n_samples=100  # Number of random weight variations
)

# Check stability of top solutions
print("Stability of top solutions:")
for i in range(5):
    stability = sensitivity['stability_top_5'][i]
    print(f"Solution {i}: appears in top 5 in {stability:.1%} of scenarios")

# Identify robust solutions (stable across weight variations)
robust_threshold = 0.8  # 80% stability
robust_solutions = np.where(sensitivity['stability_top_5'] >= robust_threshold)[0]
print(f"\nRobust solutions (>80% stability): {robust_solutions}")

Knee Point Detection

Identify the "best compromise" solutions on the Pareto front.

Basic Knee Detection

from optiscope.analysis import detect_knee_points
import numpy as np

# Get Pareto front
pareto_mask = identify_pareto_front(
    objectives=result.objectives,
    directions=result.metadata.optimization_directions
)
pareto_indices = np.where(pareto_mask)[0]
pareto_objectives = result.objectives.iloc[pareto_indices]

# Detect knee point using angle-based method
knee_indices = detect_knee_points(
    objectives=pareto_objectives,
    method="angle",  # or "distance", "tradeoff", "curvature"
    n_knees=1,
    normalize=True
)

# Map back to original indices
knee_idx = pareto_indices[knee_indices[0]]
print(f"Knee point at index: {knee_idx}")
print(f"Knee point objectives: {result.objectives.iloc[knee_idx]}")

# Create result set
result.add_set(ResultSet(
    name="knee_point",
    indices=[knee_idx],
    created_by="knee_detection",
    description="Best compromise solution (knee point)"
))

Comparing Detection Methods

# Compare different knee detection methods
methods = ["angle", "distance", "tradeoff", "curvature"]
knee_solutions = {}

for method in methods:
    knee_indices = detect_knee_points(
        objectives=pareto_objectives,
        method=method,
        n_knees=1,
        normalize=True
    )
    knee_idx = pareto_indices[knee_indices[0]]
    knee_solutions[method] = knee_idx
    print(f"{method.capitalize()} method: index {knee_idx}")
    print(f"  Objectives: {result.objectives.iloc[knee_idx]}")

# Check if methods agree
if len(set(knee_solutions.values())) == 1:
    print("\nAll methods agree on the knee point!")
else:
    print(f"\nMethods found {len(set(knee_solutions.values()))} different knee points")

Finding Knee Region

Instead of a single point, find a region of good compromise solutions:

from optiscope.analysis import find_knee_region

# Find knee region
region_indices, start, end = find_knee_region(
    objectives=pareto_objectives,
    width=0.1,  # 10% of normalized space
    method="angle",
    normalize=True
)

# Map back to original indices
region_original_indices = pareto_indices[region_indices]

print(f"Knee region contains {len(region_indices)} solutions")
print(f"Region spans from index {start} to {end}")

# Create result set
result.add_set(ResultSet(
    name="knee_region",
    indices=region_original_indices.tolist(),
    created_by="knee_region_detection",
    description=f"Knee region with {len(region_indices)} compromise solutions"
))

Combining Analysis Tools

The real power comes from combining multiple analysis tools in a workflow.

Complete Analysis Workflow

from optiscope.analysis import (
    identify_pareto_front,
    adaptive_smart_pareto_filter,
    topsis,
    detect_knee_points
)
import numpy as np

# Step 1: Identify Pareto front
print("Step 1: Identifying Pareto front...")
pareto_mask = identify_pareto_front(
    objectives=result.objectives,
    directions=result.metadata.optimization_directions
)
pareto_indices = np.where(pareto_mask)[0]
print(f"  Found {len(pareto_indices)} Pareto-optimal solutions")

# Step 2: Apply Smart Pareto Filter
print("\nStep 2: Applying Smart Pareto Filter...")
filtered_indices = adaptive_smart_pareto_filter(
    objectives=result.objectives.iloc[pareto_indices],
    target_reduction=0.3,  # Reduce to 30% of Pareto front
    min_points=10,
    normalize=True
)
final_indices = pareto_indices[filtered_indices]
print(f"  Reduced to {len(final_indices)} representative solutions")

# Step 3: Rank with TOPSIS
print("\nStep 3: Ranking with TOPSIS...")
scores = topsis(
    objectives=result.objectives.iloc[final_indices],
    weights=[0.4, 0.3, 0.3],
    directions=result.metadata.optimization_directions
)

# Get top 3 solutions
top_3_in_filtered = np.argsort(scores)[-3:][::-1]
top_3_indices = final_indices[top_3_in_filtered]

print(f"\nTop 3 recommended solutions:")
for i, idx in enumerate(top_3_indices):
    print(f"{i+1}. Index {idx}: TOPSIS score = {scores[top_3_in_filtered[i]]:.3f}")
    print(f"   Objectives: {result.objectives.iloc[idx]}")

# Step 4: Detect knee point
print("\nStep 4: Detecting knee point...")
knee_indices = detect_knee_points(
    objectives=result.objectives.iloc[final_indices],
    method="angle",
    n_knees=1,
    normalize=True
)
knee_idx = final_indices[knee_indices[0]]
print(f"  Knee point at index: {knee_idx}")
print(f"  Objectives: {result.objectives.iloc[knee_idx]}")

# Create result sets for each stage
result.add_set(ResultSet(
    name="pareto_front",
    indices=pareto_indices.tolist(),
    created_by="analysis_workflow",
    description="All Pareto-optimal solutions"
))

result.add_set(ResultSet(
    name="filtered_pareto",
    indices=final_indices.tolist(),
    created_by="analysis_workflow",
    description="Representative Pareto subset"
))

result.add_set(ResultSet(
    name="top_3_recommended",
    indices=top_3_indices.tolist(),
    created_by="analysis_workflow",
    description="Top 3 solutions by TOPSIS"
))

result.add_set(ResultSet(
    name="knee_point",
    indices=[knee_idx],
    created_by="analysis_workflow",
    description="Best compromise solution"
))

print(f"\nAnalysis complete! Created {len(result.sets)} result sets.")

Pareto Front + TOPSIS on Filtered Set

# A common workflow: filter Pareto front, then rank with TOPSIS

# 1. Get Pareto front
pareto_mask = identify_pareto_front(
    objectives=result.objectives,
    directions=result.metadata.optimization_directions
)
pareto_indices = np.where(pareto_mask)[0]

# 2. Filter to manageable size
filtered_indices = adaptive_smart_pareto_filter(
    objectives=result.objectives.iloc[pareto_indices],
    target_reduction=0.4,
    min_points=15,
    max_points=30,
    normalize=True
)
final_indices = pareto_indices[filtered_indices]

# 3. Rank with TOPSIS
scores = topsis(
    objectives=result.objectives.iloc[final_indices],
    weights=[0.5, 0.3, 0.2],  # Adjust to your preferences
    directions=result.metadata.optimization_directions
)

# 4. Select best solution
best_in_filtered = np.argmax(scores)
best_idx = final_indices[best_in_filtered]

print(f"Recommended solution: index {best_idx}")
print(f"TOPSIS score: {scores[best_in_filtered]:.3f}")
print(f"Objectives: {result.objectives.iloc[best_idx]}")
print(f"Design variables: {result.design_variables.iloc[best_idx]}")

Tips and Best Practices

When to Use Each Tool

  • Pareto Front Identification: Always start here for multi-objective problems to identify the best trade-off solutions.
  • Smart Pareto Filter: Use when your Pareto front has >50 solutions and you need cleaner visualization or a manageable set for decision-making.
  • TOPSIS: Use when you have clear preferences (weights) for objectives and need to select a single best solution or rank solutions.
  • Knee Detection: Use when you want to find the "best compromise" without specifying weights, especially for 2-3 objective problems.

Choosing TOPSIS Weights

# Equal weights (no preference)
weights = [1/3, 1/3, 1/3]

# Prioritize first objective
weights = [0.6, 0.2, 0.2]

# Normalize weights (they should sum to 1)
weights = np.array([2, 1, 1])
weights = weights / weights.sum()  # [0.5, 0.25, 0.25]

Handling Large Datasets

For very large optimization results (>100,000 solutions):

# 1. Pre-filter to reduce size
# Keep only top 20% for each objective
masks = []
for i in range(result.objectives.shape[1]):
    threshold = np.percentile(result.objectives[:, i], 20)
    if result.metadata.optimization_directions[i] == "minimize":
        masks.append(result.objectives[:, i] <= threshold)
    else:
        masks.append(result.objectives[:, i] >= threshold)

combined_mask = np.any(masks, axis=0)
filtered_result = result.objectives[combined_mask]

# 2. Then apply Pareto identification
pareto_mask = identify_pareto_front(
    objectives=filtered_result,
    directions=result.metadata.optimization_directions
)